← Terug naar artikelen
June 12, 2026
5 min leestijd

Conformal Prediction voor Risicobewuste Positiegrootte

Conformal Prediction voor Risicobewuste Positiegrootte
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

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

Verdeling van nonconformity- (residu-)scores met een kwantielgrens gemarkeerd — de kern van conformal kalibratie

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:

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

waarbij μ^\hat{\mu} elke puntvoorspeller is (lineaire regressie, random forest, neuraal netwerk — het maakt niet uit) en (Xi,Yi)(X_i, Y_i) een datapunt is.

Het kerninzicht: als de datapunten (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) uitwisselbaar zijn, dan is de rangorde van Rn+1R_{n+1} onder R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} uniform verdeeld over {1,,n+1}\{1, \ldots, n+1\}. Dit is een puur combinatorisch feit — het vereist geen aannames over de verdeling van XX of YY.

Vanuit deze rangorde-uniformiteit kunnen we predictie-intervallen construeren met eindige-steekproefdekking.

Split Conformal Prediction

Split conformal prediction: data gesplitst in training- en kalibratiedelen, de kalibratieresiduen produceren predictie-intervallen

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 Dtrain\mathcal{D}_{\text{train}} en een kalibratieset Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Stap 2. Fit een willekeurig model μ^\hat{\mu} op Dtrain\mathcal{D}_{\text{train}}.

Stap 3. Bereken nonconformity scores op de kalibratieset:

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

Stap 4. Voor een gewenst miscoverage-niveau α(0,1)\alpha \in (0, 1), neem q^\hat{q} als het (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} empirische kwantiel van R1,,RnR_1, \ldots, R_n. Concreet is dit het (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-de kleinste residu (en q^=+\hat{q} = +\infty wanneer (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, d.w.z. voor zeer kleine nn).

Stap 5. Het predictie-interval voor een nieuw punt Xn+1X_{n+1} is:

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]

De Dekkingsgarantie

Onder uitwisselbaarheid van de kalibratiedata en het nieuwe testpunt:

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

Dit is een eindige-steekproefgarantie — geen asymptotische benadering. Ze geldt voor elk model μ^\hat{\mu}, elke verdeling van de data en elke steekproefgrootte nn. Als μ^\hat{\mu} 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: 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}, 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-tt 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 yy van Yn+1Y_{n+1}:

  1. Voeg (Xn+1,y)(X_{n+1}, y) toe aan de dataset.
  2. Herfit het model op de uitgebreide dataset.
  3. Bereken alle nonconformity scores.
  4. Neem yy op in de predictieverzameling als de score voor (Xn+1,y)(X_{n+1}, y) niet te extreem is.

De predictieverzameling is:

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

waarbij RiyR_i^y 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 yy 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 μ^i\hat{\mu}_{-i} het model aanduiden dat getraind is op alle data behalve punt ii. Definieer de LOO nonconformity score met het enkele absolute residu:

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

Het jackknife+ predictie-interval wordt dan opgebouwd uit de LOO-voorspellingen op het testpunt, verbreed door deze residuen:

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]

Hier duidt q1α+{vi}q_{1-\alpha}^{+}\{v_i\} de (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-de kleinste waarde van de verzameling {vi}i=1n\{v_i\}_{i=1}^n aan, en qα{vi}q_{\alpha}^{-}\{v_i\} de α(n+1)\lfloor \alpha(n+1)\rfloor-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 μ^i+Ri\hat{\mu}_{-i} + R_i zou de ondergrens boven de voorspelling plaatsen, wat fout is.

De jackknife+ biedt een dekkingsgarantie van ten minste 12α1 - 2\alpha (iets zwakker dan de 1α1-\alpha van split conformal), maar gebruikt alle data voor zowel training als kalibratie. In de praktijk ligt de dekking meestal dicht bij 1α1-\alpha.

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 nn modelherfits.

Het Probleem Met Financiële Tijdreeksen: Non-Exchangeability

Non-exchangeability in financiële tijdreeksen: een niet-stationaire reeks met een regimeverandering, dekking die breekt naarmate de verdeling verschuift

De standaard conformal garantie vereist uitwisselbaarheid: de gezamenlijke verdeling van (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) 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)

Adaptive conformal inference: een predictie-interval dat verbreedt en versmalt via een feedbacklus die gerealiseerde dekking naar het doel volgt

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 αt\alpha_t bij en herberekent het conformal kwantiel daaruit. Op elke tijdstap tt:

  1. Bereken de conformal drempel als het empirische (1αt)(1 - \alpha_t)-kwantiel van de huidige residuenverzameling (de kalibratiescores, plus eventuele tot dusver gerealiseerde scores): q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Observeer kenmerken XtX_t, produceer het interval 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. Observeer de werkelijke waarde YtY_t en bereken de foutindicator errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Werk het niveau bij (niet de breedte):

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

waarbij γ>0\gamma > 0 een stapgrootte is en α\alpha de doel-miscoverage. Als een interval miste (errt=1\text{err}_t = 1), krimpt αt\alpha_t, wat het volgende kwantiel omhoog duwt en het interval verbreedt; als het dekte, groeit αt\alpha_t en verkleinen de intervallen. Cruciaal is dat γ\gamma hier in kansmaateenheden is — het duwt een niveau in [0,1][0,1], niet de drempel in ruwe rendementseenheden — zodat dezelfde γ\gamma zich verstandig gedraagt of residuen nu van de orde 10310^{-3} zijn of niet.

Dekkingsgarantie Voor ACI

ACI biedt een langetermijn-dekkingsgarantie die niet afhangt van een verdelingsmodel:

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}

Omdat αt\alpha_t geclipt wordt tot [0,1][0,1], is de teller begrensd door 1/γ1/\gamma maal een constante, dus de rechterkant is O(1/T)O(1/T) en de empirische miscoverage-frequentie convergeert naar α\alpha. De precieze uitspraak: ACI garandeert dat de langetermijn-empirische miscoverage-frequentie convergeert naar α\alpha 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 γ\gamma. In plaats van γ\gamma vast te zetten, houden ze een kandidatenset Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} bij en combineren ze deze via een expert-aggregatieregel, waarbij de γ\gamma wordt bevoordeeld wiens recente dekking het dichtst bij het doel ligt.

Dit pakt een praktisch probleem aan: een grote γ\gamma past zich snel aan regimeveranderingen aan maar produceert volatiele intervalbreedtes; een kleine γ\gamma 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

Het mappen van een gekalibreerd onzekerheidsinterval naar positiegrootte: een smal interval drijft een grote positie, een breed interval een kleine positie

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 [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t], is de volledige breedte wt=2q^tw_t = 2\hat{q}_t. Om formules en code consistent te houden, meten we alles overal tegen de volledige breedte wtw_t.

Inverse-Breedte Sizing

De eenvoudigste aanpak: bepaal de grootte omgekeerd evenredig met de intervalbreedte.

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

waarbij kk 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 (size1/σ\text{size} \propto 1/\sigma), 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:

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

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,

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

dan bevat het (1α)(1-\alpha) predictie-interval zowel positieve als negatieve rendementen — het gerealiseerde rendement kan plausibel het tegenovergestelde teken van je voorspelling hebben. Definieer een minimale edge-drempel θ\theta en handel alleen wanneer et>θe_t > \theta. Merk op dat de geometrische inhoud van "ete_t groot genoeg zodat het interval nul niet meer overspant" precies et>1/2e_t > 1/2 is (aangezien het interval nul vrijmaakt wanneer μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Kies θ\theta op de daadwerkelijke schaal van et=μ^/wte_t = |\hat{\mu}|/w_t via backtesting; voor residuen van dagrendementen ligt ete_t meestal ver onder 1/21/2, dus een piepklein θ\theta 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 f=pb(1p)bf^* = \frac{pb - (1-p)}{b}. Maar ff^* is al een complete, begrensde fractie afgeleid van een winkans pp en een uitbetalingsratio bb; het vermenigvuldigen ervan met een onbegrensde ratio zoals μ^/q^\hat{\mu}/\hat{q} heeft geen besliskundige rechtvaardiging — het kan groter zijn dan 1 of van teken wisselen onafhankelijk van ff^*, en het telt de edge die ff^* al vastlegt dubbel. Dus presenteren we geen "conformal Kelly"-vermenigvuldiger.

Als je Kelly wilt aansturen vanuit het interval, moet je daadwerkelijk pp en bb 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 pP(rendement>0)p \approx \mathbb{P}(\text{rendement} > 0) 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 et=μ^/wte_t = |\hat{\mu}|/w_t 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 αt\alpha_t 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.quantile interpoleert standaard, wat net onder de vereiste rangorde kan vallen en onderdekking kan veroorzaken; geef method="higher" mee (equivalent aan "inverted_cdf").
  • Wanneer de vereiste rangorde groter is dan nn (kleine nn, hoge doeldekking), is de correcte drempel ++\infty (interval = de gehele lijn), geen klem op het grootste residu. Klemmen doorbreekt stilzwijgend de 1αt\ge 1-\alpha_t-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 Yμ^(X)|Y - \hat{\mu}(X)| 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 α/2\alpha/2 en 1α/21 - \alpha/2, 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 wtw_t is de echte boven-min-onder-breedte — blijf wtw_t overal als noemer gebruiken.)
  • Genormaliseerde scores: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), waarbij σ^\hat{\sigma} 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 nn, maar de intervalbreedte neemt af naarmate nn toeneemt. Voor zeer kleine nn bestaat de vereiste orde-statistiek mogelijk niet, in welk geval de eerlijke drempel ++\infty is (een oninformatief maar geldig interval) — klemmen op het grootste residu maakt de garantie stilzwijgend ongeldig. Voor praktische trading:

  • n100n \geq 100 kalibratiepunten geeft redelijk smalle intervallen.
  • n500n \geq 500 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 μ^\hat{\mu} kan verouderen. Twee benaderingen:

  1. Periodiek herscholen (bijv. maandelijks) en het conformal kwantiel opnieuw kalibreren.
  2. 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:

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

Dit zorgt ervoor dat je alleen handelt wanneer de verwachte netto-edge een drempel overschrijdt geschaald met de conformal breedte — met dezelfde wtw_t-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+: O(n)O(n) 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:

  1. Marginale, geen conditionele dekking. De garantie is dat P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha marginaal — gemiddeld over de willekeur in zowel XX als YY. Voor een specifieke conditioneringswaarde X=xX = x kan de lokale dekking boven of onder 1α1 - \alpha liggen. Conformalized quantile regression pakt dit gedeeltelijk aan.

  2. 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.

  3. Intervalbreedte is geen kansdichtheid. Een conformal interval vertelt je waar YY zal vallen met kans 1α1-\alpha, 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 pp en bb eruit een extra, expliciete aanname vereist.

  4. 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 nn 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 pp en bb 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.
Disclaimer: De informatie in dit artikel is uitsluitend bedoeld voor educatieve en informatieve doeleinden en vormt geen financieel, beleggings- of handelsadvies. Het handelen in cryptovaluta brengt een aanzienlijk risico op verlies met zich mee.

Auteurs

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

Blijf de markt voor

Abonneer je op onze nieuwsbrief voor exclusieve AI-handelsinzichten, marktanalyses en platformupdates.

We respecteren je privacy. Je kunt je op elk moment afmelden.