← Zurück zu den Artikeln
June 12, 2026
5 min read

Konforme Prädiktion für risikobewusste Positionsgrößenbestimmung

Konforme Prädiktion für risikobewusste Positionsgrößenbestimmung
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Part 4 of 4 · Collection
Deep Learning for Markets

Jede Formel zur Positionsgrößenbestimmung braucht eine Schätzung der Unsicherheit. Das Kelly-Kriterium braucht eine Gewinnwahrscheinlichkeit und ein Auszahlungsverhältnis (siehe Das Kelly-Kriterium für Strategien). Mean-Variance-Optimierung braucht eine Kovarianzmatrix. VaR braucht eine Renditeverteilung. All das erfordert Annahmen über den datengenerierenden Prozess — Annahmen, die Finanzmärkte routinemäßig verletzen.

Konforme Prädiktion bietet etwas anderes: Prognoseintervalle mit Abdeckungsgarantien für endliche Stichproben, ohne jegliche parametrische Verteilungsannahmen. Wenn Sie 90 % Abdeckung verlangen, bekommen Sie mindestens 90 % Abdeckung — unabhängig davon, ob die Renditen gaußverteilt, fat-tailed, schief oder heteroskedastisch sind. Die einzige Voraussetzung ist Austauschbarkeit (exchangeability) — oder schwächere Bedingungen, wie wir noch sehen werden.

Dieser Beitrag behandelt die Theorie, die wichtigsten Varianten und eine praktische Implementierung für die Positionsgrößenbestimmung in Python.

Die Kernidee: Nonkonformitäts-Scores

Verteilung der Nonkonformitäts-(Residual-)Scores mit markiertem Quantil-Schwellenwert — der Kern der konformen Kalibrierung

Konforme Prädiktion funktioniert, indem gemessen wird, wie "seltsam" eine neue Beobachtung im Verhältnis zu vergangenen Daten ist. Die Seltsamkeit wird durch einen Nonkonformitäts-Score quantifiziert — eine beliebige Funktion, die misst, wie schlecht ein Datenpunkt zum Muster der übrigen Daten passt.

Für Regression (Vorhersage eines kontinuierlichen Werts wie Renditen) ist der einfachste Nonkonformitäts-Score das absolute Residuum:

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

wobei μ^\hat{\mu} ein beliebiger Punktschätzer ist (lineare Regression, Random Forest, neuronales Netz — es spielt keine Rolle) und (Xi,Yi)(X_i, Y_i) ein Datenpunkt ist.

Die zentrale Erkenntnis: Wenn die Datenpunkte (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) austauschbar sind, dann ist der Rang von Rn+1R_{n+1} unter R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} gleichverteilt über {1,,n+1}\{1, \ldots, n+1\}. Dies ist eine rein kombinatorische Tatsache — sie erfordert keinerlei Annahmen über die Verteilung von XX oder YY.

Aus dieser Rang-Gleichverteilung können wir Prognoseintervalle mit endlicher-Stichproben-Abdeckung konstruieren.

Split-Conformal-Prädiktion

Split-Conformal-Prädiktion: Daten aufgeteilt in Trainings- und Kalibrierungs-Folds, die Kalibrierungsresiduen erzeugen Prognoseintervalle

Split-Conformal-Prädiktion (Papadopoulos et al., 2002; Lei et al., 2018) ist die praktikabelste Variante. Der Algorithmus ist einfach:

Schritt 1. Teile die Daten in ein Trainingsset Dtrain\mathcal{D}_{\text{train}} und ein Kalibrierungsset Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Schritt 2. Fitte ein beliebiges Modell μ^\hat{\mu} auf Dtrain\mathcal{D}_{\text{train}}.

Schritt 3. Berechne Nonkonformitäts-Scores auf dem Kalibrierungsset:

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

Schritt 4. Für ein gewünschtes Fehlabdeckungsniveau α(0,1)\alpha \in (0, 1) nimm q^\hat{q} als das (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n}-empirische Quantil von R1,,RnR_1, \ldots, R_n. Konkret ist das das (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-kleinste Residuum (und q^=+\hat{q} = +\infty, wann immer (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, d. h. bei sehr kleinem nn).

Schritt 5. Das Prognoseintervall für einen neuen Punkt Xn+1X_{n+1} ist:

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]

Die Abdeckungsgarantie

Unter Austauschbarkeit der Kalibrierungsdaten und des neuen Testpunkts:

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

Dies ist eine Garantie für endliche Stichproben — keine asymptotische Näherung. Sie gilt für jedes Modell μ^\hat{\mu}, jede Datenverteilung und jede Stichprobengröße nn. Ist μ^\hat{\mu} ein miserabler Prädiktor, werden die Intervalle einfach breiter. Die Abdeckungsgarantie bleibt trotzdem bestehen.

Es gibt auch eine Obergrenze, wenn die Scores keine Bindungen (ties) aufweisen: 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}, sodass die Abdeckung nicht unnötig konservativ ist.

Warum das für den Handel wichtig ist

Traditionelle Prognoseintervalle, etwa aus einer linearen Regression, setzen gaußsche Fehler voraus. Ein gaußsches Intervall, das auf dem Großteil der Daten kalibriert wurde, kann die Ränder stark falsch einschätzen, wenn die wahren Residuen dickschwänzig sind (z. B. Student-tt mit wenigen Freiheitsgraden): Die zentrale Masse ist dünner als bei Gauß, sodass ein varianz-angepasstes gaußsches Intervall nahe der Mitte überdeckt, aber in den Rändern unterdeckt — ein an die Ränder angepasstes Intervall macht das Gegenteil. Der Punkt ist nicht eine einzelne magische Zahl — sondern dass die realisierte Abdeckung eines parametrischen Intervalls von einer Verteilungsannahme abhängt, die Sie nicht überprüft haben.

Konforme Prognoseintervalle umgehen dies. Sie verbreitern sich automatisch, wenn das Modell unsicher ist, und behalten die marginale Abdeckung bei, unabhängig von der wahren Fehlerverteilung. Für einen Trader bedeutet das:

  • Wenn Sie Positionen umgekehrt proportional zur Intervallbreite bemessen, reduzieren Sie automatisch das Exposure, wenn das Modell unsicher ist.
  • Die Abdeckungsgarantie bedeutet, dass Ihre Risikoschätzungen ehrlich sind — wenn Sie sagen "90 % der realisierten Renditen werden in dieses Intervall fallen", ist diese Aussage statistisch gültig (marginal, unter Austauschbarkeit).

Full Conformal und Jackknife+

Split-Conformal ist einfach, verschwendet aber Daten: Das Kalibrierungsset kann nicht zum Training verwendet werden. Zwei Alternativen adressieren dies.

Full-Conformal-Prädiktion

Full-Conformal-Prädiktion (Vovk et al., 2005) nutzt alle Daten sowohl für Training als auch Kalibrierung. Für jeden Kandidatenwert yy von Yn+1Y_{n+1}:

  1. Erweitere den Datensatz um (Xn+1,y)(X_{n+1}, y).
  2. Fitte das Modell auf dem erweiterten Datensatz neu.
  3. Berechne alle Nonkonformitäts-Scores.
  4. Nimm yy in die Prognosemenge auf, wenn der Score für (Xn+1,y)(X_{n+1}, y) nicht zu extrem ist.

Die Prognosemenge ist:

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

wobei RiyR_i^y die mit dem erweiterten Datensatz berechneten Nonkonformitäts-Scores sind.

Full Conformal liefert die engsten Intervalle, ist aber für die meisten Modelle rechnerisch untragbar — man muss das Modell für jeden Kandidaten yy auf einem Gitter neu fitten. Für eine Renditevorhersage könnte das Tausende von Refits pro Vorhersage bedeuten.

Jackknife+ (Barber et al., 2021)

Der Jackknife+ schafft einen Ausgleich. Er nutzt Leave-One-Out-(LOO-)Residuen, berücksichtigt dabei aber die Variabilität des gefitteten Modells über die LOO-Folds hinweg.

Sei μ^i\hat{\mu}_{-i} das Modell, das auf allen Daten außer Punkt ii trainiert wurde. Definiere den LOO-Nonkonformitäts-Score mit dem einzelnen absoluten Residuum:

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

Das Jackknife+-Prognoseintervall wird dann aus den LOO-Vorhersagen am Testpunkt gebildet, erweitert um diese 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 bezeichnet q1α+{vi}q_{1-\alpha}^{+}\{v_i\} den (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-kleinsten Wert der Menge {vi}i=1n\{v_i\}_{i=1}^n, und qα{vi}q_{\alpha}^{-}\{v_i\} den α(n+1)\lfloor \alpha(n+1)\rfloor-kleinsten Wert. Die untere Grenze subtrahiert das Residuum von jeder LOO-Vorhersage; die obere Grenze addiert es. Diese Asymmetrie ist der ganze Punkt — beide Grenzen auf μ^i+Ri\hat{\mu}_{-i} + R_i zu reduzieren, würde die untere Grenze über die Vorhersage setzen, was falsch ist.

Der Jackknife+ liefert eine Abdeckungsgarantie von mindestens 12α1 - 2\alpha (etwas schwächer als das 1α1-\alpha von Split-Conformal), nutzt aber alle Daten sowohl für Training als auch Kalibrierung. In der Praxis liegt die Abdeckung typischerweise nahe bei 1α1-\alpha.

Für Handelsmodelle, die auf begrenzten Daten trainiert werden (z. B. regimespezifische Modelle mit nur wenigen hundert Beobachtungen), ist der Jackknife+ oft die beste Wahl — er opfert keine knappen Daten für die Kalibrierung. Die Kosten sind nn Modell-Refits.

Das Problem mit Finanzzeitreihen: Nicht-Austauschbarkeit

Nicht-Austauschbarkeit in Finanzzeitreihen: eine nicht-stationäre Reihe mit Regimewechsel, die Abdeckung bricht ein, während die Verteilung driftet

Die Standard-Konformitätsgarantie erfordert Austauschbarkeit: die gemeinsame Verteilung von (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) ist invariant unter Permutationen. Für i.i.d.-Daten gilt dies trivial.

Finanzzeitreihen sind nicht austauschbar. Renditen zeigen:

  • Volatilitätsclustering: Perioden hoher Volatilität folgen auf Perioden hoher Volatilität (GARCH-Effekte).
  • Momentum und Mean Reversion: Autokorrelation in Renditen oder quadrierten Renditen.
  • Regimewechsel: Die Verteilung verschiebt sich mit der Zeit (Bullen- vs. Bärenmärkte).

Wenn Sie Split-Conformal naiv auf eine Zeitreihe anwenden — mit einer zufälligen Kalibrierungsaufteilung —, verletzen Sie die zeitliche Struktur. Die Kalibrierungs-Scores aus einem ruhigen Jahr 2017 spiegeln nicht die Unsicherheit eines volatilen Jahres 2020 wider. Ihre Abdeckungsgarantie bricht zusammen.

Adaptive Conformal Inference (ACI)

Adaptive konforme Inferenz: ein Prognoseintervall, das sich über eine Rückkopplungsschleife verbreitert und verengt, die die realisierte Abdeckung auf das Ziel zubewegt

Gibbs und Candes (2021, NeurIPS) führten Adaptive Conformal Inference (ACI) ein, um Verteilungsverschiebungen und nicht-austauschbare Daten zu handhaben. Die Idee ist elegant: Statt ein festes Abdeckungsniveau zu verwenden, passt man das Ziel-Fehlabdeckungsniveau online an, basierend darauf, ob die jüngsten Intervalle das tatsächliche Ergebnis abgedeckt haben, und leitet das Quantil bei jedem Schritt neu aus der Score-Verteilung ab.

Der ACI-Algorithmus

ACI verschiebt nicht direkt die Intervallbreite. Es unterhält einen adaptiven Fehlabdeckungsparameter αt\alpha_t und berechnet daraus das konforme Quantil neu. Bei jedem Zeitschritt tt:

  1. Berechne den konformen Schwellenwert als das empirische (1αt)(1 - \alpha_t)-Quantil der aktuellen Residuenmenge (die Kalibrierungs-Scores plus alle bisher realisierten Scores): q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Beobachte die Merkmale XtX_t, erzeuge das Intervall 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. Beobachte den tatsächlichen Wert YtY_t und berechne den Fehlerindikator errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Aktualisiere das Niveau (nicht die Breite):

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

wobei γ>0\gamma > 0 eine Schrittweite ist und α\alpha die Ziel-Fehlabdeckung. Wenn ein Intervall verfehlte (errt=1\text{err}_t = 1), schrumpft αt\alpha_t, was das nächste Quantil nach oben drückt und das Intervall verbreitert; wenn es abdeckte, wächst αt\alpha_t und die Intervalle verengen sich. Entscheidend: γ\gamma ist hier in Wahrscheinlichkeitseinheiten — es verschiebt ein Niveau in [0,1][0,1], nicht den Schwellenwert in rohen Rendite-Einheiten — sodass dasselbe γ\gamma sinnvoll funktioniert, egal ob Residuen in der Größenordnung von 10310^{-3} liegen oder nicht.

Abdeckungsgarantie für ACI

ACI liefert eine langfristige Abdeckungsgarantie, die nicht von einem Verteilungsmodell abhängt:

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}

Da αt\alpha_t auf [0,1][0,1] begrenzt ist, ist der Zähler durch 1/γ1/\gamma mal eine Konstante beschränkt, sodass die rechte Seite O(1/T)O(1/T) ist und die empirische Fehlabdeckungshäufigkeit gegen α\alpha konvergiert. Die präzise Aussage: ACI garantiert, dass die langfristige empirische Fehlabdeckungshäufigkeit für beliebige (auch adversarielle) Sequenzen gegen α\alpha konvergiert, sofern die angepassten Niveaus beschränkt bleiben — was das Clipping erzwingt. Es ist eine Garantie für die Abdeckungshäufigkeit, nicht für die Informativität der Intervalle: unter einer wirklich adversariellen Sequenz können die Intervalle uninformativ breit werden, während sie das Abdeckungsziel dennoch erreichen.

Dynamically-Tuned ACI (DtACI)

Gibbs und Candes (2024, JMLR) verfeinerten ACI mit dynamischer Anpassung der Schrittweite γ\gamma. Statt γ\gamma fest zu wählen, unterhalten sie ein Kandidatenset Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} und kombinieren diese über eine Expertenaggregationsregel, wobei das γ\gamma bevorzugt wird, dessen jüngste Abdeckung dem Ziel am nächsten kommt.

Dies adressiert ein praktisches Problem: Ein großes γ\gamma passt sich schnell an Regimewechsel an, erzeugt aber volatile Intervallbreiten; ein kleines γ\gamma ist stabil, aber langsam in der Anpassung. DtACI gleicht diese Abwägung automatisch aus.

Warum das für den Handel wichtig ist

Betrachten Sie eine Market-Making-Strategie, die ein Rendite-Prognosemodell nutzt. In ruhigen Märkten sind die konformen Intervalle eng — das Modell ist zuversichtlich, und Sie können größere Positionen eingehen. Wenn die Volatilität ansteigt (Earnings-Saison, FOMC-Ankündigungen, geopolitische Schocks), passt sich das ACI-Niveau an, und die Intervalle verbreitern sich innerhalb weniger Zeitschritte. Ihre Positionsgröße schrumpft entsprechend, ohne ein explizites Volatilitätsmodell oder eine Regime-Erkennungslogik.

Dies ist Unsicherheitsquantifizierung als erstklassiges Signal, nicht als nachträglicher Gedanke.

Positionsgrößenbestimmung mit konformen Intervallen

Abbildung eines kalibrierten Unsicherheitsintervalls auf die Positionsgröße: ein enges Intervall treibt eine große Position, ein breites eine kleine

Verbinden wir nun die konforme Prädiktion mit konkreter Positionsgrößenbestimmung. Die Schlüsselvariable ist die halbe Breite des Prognoseintervalls relativ zum symmetrischen Fall des absoluten Residuums. Mit dem symmetrischen Intervall [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t] ist die volle Breite wt=2q^tw_t = 2\hat{q}_t. Um Formeln und Code konsistent zu halten, messen wir durchgehend alles gegen die volle Breite wtw_t.

Inverse-Breite-Bemessung

Der einfachste Ansatz: Bemessung umgekehrt proportional zur Intervallbreite.

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

wobei kk eine Skalierungskonstante ist, kalibriert auf Ihr Risikobudget. Wenn das Modell zuversichtlich ist (enges Intervall), gehen Sie eine größere Position ein. Bei Unsicherheit (breites Intervall) eine kleinere.

Dies ist analog zum Volatility Targeting (size1/σ\text{size} \propto 1/\sigma), aber mit einem entscheidenden Unterschied: Die konforme Intervallbreite ist ein verteilungsfreies Unsicherheitsmaß, keine parametrische Volatilitätsschätzung. Sie erfasst die prädiktive Unsicherheit unter der Abdeckungsgarantie, nicht nur die Renditevarianz.

Edge-Ratio-Bemessung und der No-Trade-Filter

Reine Inverse-Breite-Bemessung ignoriert die Stärke des Signals selbst. Eine natürliche Verfeinerung skaliert nach dem Edge Ratio — der Punktvorhersage relativ zur Intervallbreite:

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

Dies ist ein konformes Analogon zu einem Signal-Rausch-Verhältnis: erwartete Rendite geteilt durch ein verteilungsfreies Unsicherheitsmaß. Wir nutzen es sowohl für die Bemessung als auch für einen No-Trade-Filter.

Der Filter ist prinzipienbasiert. Wenn das Intervall die Null überspannt,

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

dann schließt das (1α)(1-\alpha)-Prognoseintervall sowohl positive als auch negative Renditen ein — die realisierte Rendite kann plausibel das entgegengesetzte Vorzeichen Ihrer Vorhersage haben. Definieren Sie einen minimalen Edge-Schwellenwert θ\theta und handeln Sie nur, wenn et>θe_t > \theta. Beachten Sie, dass der geometrische Gehalt von "ete_t groß genug, dass das Intervall die Null nicht mehr überspannt" genau et>1/2e_t > 1/2 ist (da das Intervall die Null verlässt, wenn μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Wählen Sie θ\theta auf der tatsächlichen Skala von et=μ^/wte_t = |\hat{\mu}|/w_t per Backtesting; für Tagesrendite-Residuen liegt ete_t üblicherweise deutlich unter 1/21/2, sodass ein winziges θ\theta nahezu alle Trades zulassen kann und ein großes keinen einzigen. Kalibrieren Sie es auf Ihre Daten.

Zum "Conformal Kelly"

Es ist verlockend, konforme Intervalle an den Kelly-Bruch f=pb(1p)bf^* = \frac{pb - (1-p)}{b} anzuschrauben. Aber ff^* ist bereits ein vollständiger, beschränkter Bruch, der aus einer Gewinnwahrscheinlichkeit pp und einem Auszahlungsverhältnis bb abgeleitet wird; ihn mit einem unbeschränkten Verhältnis wie μ^/q^\hat{\mu}/\hat{q} zu multiplizieren, hat keine entscheidungstheoretische Rechtfertigung — es kann 1 überschreiten oder das Vorzeichen unabhängig von ff^* umkehren, und es zählt den Edge, den ff^* bereits erfasst, doppelt. Deshalb präsentieren wir kein "Conformal-Kelly"-Multiplikator.

Wenn Sie Kelly aus dem Intervall ableiten wollen, müssen Sie tatsächlich pp und bb daraus ableiten, was eine explizite Annahme über die Verteilung innerhalb des Intervalls erfordert (konforme Intervalle sagen absichtlich nichts darüber aus — siehe Einschränkungen). Zum Beispiel können Sie unter einer angenommenen Form innerhalb des Intervalls pP(Rendite>0)p \approx \mathbb{P}(\text{Rendite} > 0) und ein Auszahlungsverhältnis aus der Intervallgeometrie annähern — aber weisen Sie diese Annahme deutlich aus, denn sie führt genau das parametrische Commitment wieder ein, das konforme Prädiktion vermeiden sollte.

Die ehrliche, annahmearme Alternative besteht darin, das Edge Ratio et=μ^/wte_t = |\hat{\mu}|/w_t als fraktionale Kelly-Schrumpfung zu verwenden: größer bemessen, wenn die erwartete Rendite im Verhältnis zum Intervall groß ist, kleiner bemessen, wenn sie klein ist, und dies zusätzlich zu einer harten Obergrenze anwenden — ausdrücklich als Heuristik, nicht als "den Kelly-Bruch".

Python-Implementierung

Hier eine praktische Implementierung. Wir zeigen sowohl den Split/Prefit-Pfad als auch den zeitlichen (EnbPI-)Pfad, denn der ganze Sinn dieses Beitrags ist, dass Finanzdaten nicht austauschbar sind.

Setup und Datenvorbereitung

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

Für Split/Prefit-Conformal erfordert cv="prefit" method="base" (den naiven Split-Conformal-Schätzer). Die Option method="plus" ist der CV+/Jackknife+-Schätzer und ist mit cv="prefit" inkompatibel — er benötigt stattdessen ein Cross-Validation-Objekt. Wir verwenden hier die korrekte Kombination und standardisieren die Bemessung auf die volle Breite.

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)

Zeitreihen-Conformal mit EnbPI

Da Renditen nicht austauschbar sind, ist der obige zufällige/Prefit-Split nur eine Baseline. MAPIEs MapieTimeSeriesRegressor mit method="enbpi" (Xu & Xie, 2021) nutzt Block-Bootstrap und Residuen-Updating, konzipiert für zeitliche Abhängigkeit. Dies ist das Werkzeug, das zum eigentlichen Argument dieses Beitrags passt.

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)

Für den Live-Handel implementieren wir echtes ACI von Grund auf: das Fehlabdeckungs-Niveau αt\alpha_t pflegen, additiv aktualisieren und das Quantil bei jedem Schritt neu aus der Residuenmenge ableiten. Zwei Details für endliche Stichproben sind wichtig:

  • Verwenden Sie die Ordnungsstatistik, nicht ein interpoliertes Quantil. np.quantile interpoliert standardmäßig, was gerade unter den erforderlichen Rang fallen und zur Unterdeckung führen kann; übergeben Sie method="higher" (äquivalent "inverted_cdf").
  • Wenn der erforderliche Rang nn übersteigt (kleines nn, hohe Zielabdeckung), ist der korrekte Schwellenwert ++\infty (Intervall = die ganze Zahlengerade), keine Klammerung auf das größte Residuum. Klammerung bricht die 1αt\ge 1-\alpha_t-Garantie stillschweigend.
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 zusammen: Backtest-Loop

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

Auswertung der Ergebnisse

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 Überlegungen

Auswahl des Nonkonformitäts-Scores

Das absolute Residuum Yμ^(X)|Y - \hat{\mu}(X)| ist der Standard, setzt aber voraus, dass das Prognoseintervall symmetrisch um die Punktvorhersage sein soll. Für Finanzrenditen ergeben asymmetrische Intervalle oft mehr Sinn:

  • Conformalized Quantile Regression (CQR): Fitte Quantil-Regressoren auf den Niveaus α/2\alpha/2 und 1α/21 - \alpha/2, dann konformalisiere (Romano et al., 2019). Die Intervalle passen ihre Form an die lokale Verteilung an — breiter auf der Abwärtsseite während Drawdowns, breiter auf der Aufwärtsseite während Rallyes. (Mit CQR ist das Intervall nicht mehr symmetrisch, sodass wtw_t die tatsächliche Breite oben-minus-unten ist — verwenden Sie überall weiterhin wtw_t als Nenner.)
  • Normalisierte Scores: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), wobei σ^\hat{\sigma} eine lokale Volatilitätsschätzung ist. Dies erzeugt bedingt adaptive Intervalle — enger in Niedrigvolatilitätsregimen, breiter in Hochvolatilitätsregimen — bei gleichzeitiger Beibehaltung der marginalen Abdeckung.

Größe des Kalibrierungssets

Die Abdeckungsgarantie von Split-Conformal gilt für jede Kalibrierungssetgröße nn, aber die Intervallbreite nimmt mit wachsendem nn ab. Für sehr kleines nn existiert die erforderliche Ordnungsstatistik möglicherweise nicht; in diesem Fall ist der ehrliche Schwellenwert ++\infty (ein uninformatives, aber gültiges Intervall) — eine Klammerung auf das größte Residuum hebt die Garantie still auf. Für praktischen Handel:

  • n100n \geq 100 Kalibrierungspunkte liefern angemessen enge Intervalle.
  • n500n \geq 500 ist für stabile Quantilschätzung vorzuziehen.
  • Bei ACI dient das Kalibrierungsset nur der Initialisierung; die Online-Niveau-Updates übernehmen den Rest.

Häufigkeit des Neutrainings

Das Basismodell μ^\hat{\mu} kann veralten. Zwei Ansätze:

  1. Regelmäßig neu trainieren (z. B. monatlich) und das konforme Quantil neu kalibrieren.
  2. ACI verwenden und das adaptive Niveau die Modellveralterung kompensieren lassen — die Intervalle verbreitern sich automatisch, während die Residuen des Modells wachsen.

Option 2 ist einfacher und überraschend effektiv. Die konforme Schicht wirkt wie ein Sicherheitsnetz: Selbst wenn sich das Modell verschlechtert, konvergiert die langfristige ACI-Abdeckungshäufigkeit weiterhin gegen das Ziel.

Transaktionskosten

Konforme Intervalle interagieren auf nützliche Weise mit Transaktionskosten. Wenn Intervalle breit sind (hohe Unsicherheit), sind Positionen klein, sodass der Umsatz gering ist. Wenn sich Intervalle verengen (das Modell ist zuversichtlich), wachsen die Positionen — aber das Modell liegt mit größerer Wahrscheinlichkeit richtig, sodass sich der Umsatz lohnt.

Sie können Transaktionskosten auch direkt in den No-Trade-Filter einbeziehen:

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

Dies stellt sicher, dass Sie nur handeln, wenn der erwartete Nettoedge einen Schwellenwert übersteigt, der auf die konforme Breite skaliert ist — mit demselben wtw_t-Nenner wie überall sonst.

Vergleich mit traditionellen Methoden

Eigenschaft Gauß-KI Bootstrap-KI Konformes PI
Verteilungsannahme Normale Fehler i.i.d. + asymptotisch Keine (Austauschbarkeit)
Endliche-Stichproben-Garantie Nein Nein Ja
Funktioniert mit jedem Modell Nein Ja Ja
Passt sich an Heteroskedastizität an Nein Teilweise Mit CQR / normalisierten Scores
Handhabt Verteilungsverschiebung Nein Nein ACI / EnbPI-Variante
Rechenkosten Niedrig Mittel Split: niedrig; Jackknife+: O(n)O(n) Refits; Full: untragbar

Der Bootstrap ist nur in seiner Garantie "asymptotisch"; er setzt weiterhin i.i.d./austauschbare Daten und Glätte voraus, sodass seine Verteilungsannahme-Zelle nicht "annahmefrei" ist. Und die einzelne Spalte "konform" verbirgt sehr unterschiedliche Kosten über die Varianten hinweg, die die Kostenzeile nun aufschlüsselt.

Einschränkungen

Konforme Prädiktion ist keine Magie. Wichtige Einschränkungen für den Handel:

  1. Marginale, keine bedingte Abdeckung. Die Garantie ist, dass P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha marginal gilt — gemittelt über die Zufälligkeit in sowohl XX als auch YY. Für einen spezifischen Bedingungswert X=xX = x kann die lokale Abdeckung über oder unter 1α1 - \alpha liegen. Conformalized Quantile Regression adressiert dies teilweise.

  2. Austauschbarkeit ist eine echte Voraussetzung. Für Split-Conformal müssen die Kalibrierungsdaten und der Testpunkt austauschbar sein. Finanzdaten sind es nicht. ACI und EnbPI lockern dies zu einer langfristigen Garantie, aber die kurzfristige Abdeckung kann abweichen.

  3. Intervallbreite ist keine Wahrscheinlichkeitsdichte. Ein konformes Intervall sagt Ihnen, wo YY mit Wahrscheinlichkeit 1α1-\alpha liegen wird, aber es sagt nichts über die Verteilung innerhalb des Intervalls aus. Es ist kein Ersatz für eine vollständige prädiktive Verteilung — genau deshalb erfordert die Ableitung eines Kelly-pp und -bb daraus eine zusätzliche, explizite Annahme.

  4. Müll rein, breiter raus. Ein schlechtes Basismodell erzeugt breite Intervalle. Konforme Prädiktion garantiert Abdeckung, aber nicht Nützlichkeit. Wenn Ihr Modell keine prädiktive Kraft hat, werden die Intervalle so breit, dass der Positionsgrößen-Bemesser nie handelt.

Zusammenfassung

Konforme Prädiktion bietet ein rigoroses, verteilungsfreies Framework für Unsicherheitsquantifizierung, das sich natürlich in die Positionsgrößenbestimmung einfügt:

  • Split Conformal für statische, Offline-Kalibrierung mit endlicher-Stichproben-Abdeckung.
  • Jackknife+, wenn Kalibrierungsdaten knapp sind und Sie alle Beobachtungen nutzen wollen (auf Kosten von nn Refits).
  • Adaptive konforme Inferenz / EnbPI für Online-Handel mit nicht-stationären Märkten.
  • Positionsgrößenbestimmung über inverse Intervallbreite und einen Edge-Ratio-No-Trade-Filter — und, falls Sie zu Kelly übergehen, nur nachdem Sie pp und bb ehrlich abgeleitet haben, nicht über einen unbegründeten Multiplikator.

Der wesentliche Vorteil gegenüber parametrischen Alternativen: Sie müssen nie eine parametrische Verteilungsannahme spezifizieren oder validieren. Die Intervalle sind konstruktionsbedingt ehrlich (marginal, unter Austauschbarkeit). Für einen systematischen Trader bedeutet das eine Risikoquelle weniger im Modell — und in einem Geschäft, in dem Modellrisiko existenziell ist, zählt das.


Referenzen:

  • Papadopoulos, H., Proedrou, K., Vovk, V., Gammerman, A. (2002). Inductive confidence machines for regression. ECML.
  • Vovk, V., Gammerman, A., Shafer, G. (2005). Algorithmic Learning in a Random World. Springer.
  • Lei, J., G'Sell, M., Rinaldo, A., Tibshirani, R.J., Wasserman, L. (2018). Distribution-free predictive inference for regression. JASA.
  • Xu, C., Xie, Y. (2021). Conformal prediction interval for dynamic time-series (EnbPI). ICML.
  • Barber, R.F., Candes, E.J., Ramdas, A., Tibshirani, R.J. (2021). Predictive inference with the jackknife+. Annals of Statistics.
  • Gibbs, I., Candes, E.J. (2021). Adaptive conformal inference under distribution shift. NeurIPS.
  • Gibbs, I., Candes, E.J. (2024). Conformal inference for online prediction with arbitrary distribution shifts. JMLR.
  • Romano, Y., Patterson, E., Candes, E.J. (2019). Conformalized quantile regression. NeurIPS.
  • Cordier, T. et al. (2022). MAPIE: an open-source library for distribution-free uncertainty quantification. arXiv:2207.12274.
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Dem Markt einen Schritt voraus

Abonniere unseren Newsletter für exklusive KI-Trading-Einblicke, Marktanalysen und Plattform-Updates.

Wir respektieren deine Privatsphäre. Jederzeit abbestellbar.