← Alle Insights

Ornstein-Uhlenbeck Mean Reversion: vom Modell zur Strategie.

Der Ornstein-Uhlenbeck-Prozess ist das mathematische Rückgrat fast jeder Mean-Reversion- Strategie. Wer ihn versteht, kann Halbwertszeiten korrekt schätzen, optimale Trading- Schwellen ableiten und vor allem erkennen, wann ein Spread kein stationärer OU-Prozess mehr ist — bevor das die Strategie kostet.

Das Modell in einer Zeile.

Die stochastische Differentialgleichung des Ornstein-Uhlenbeck-Prozesses lautet:

dX_t = θ · (μ − X_t) · dt + σ · dW_t

Drei Parameter, jeder mit konkreter Bedeutung. μ ist der langfristige Mittelwert, zu dem der Prozess zurückkehrt. θ ist die Geschwindigkeit dieser Rückkehr (mean-reversion rate). σ ist die Volatilität der Schocks. dW_t ist Brownsche Bewegung — reines Rauschen.

Das ist das einfachste stationäre stochastische Modell, das Sie überhaupt finden. Genau deshalb passt es als Erstes auf praktisch jeden Spread, der mean-revertet: Pairs-Spread, PCA-Residuum, Basis zwischen Future und Underlying, Term-Structure- Spread.

Wichtige Eigenschaften, die Sie kennen müssen.

Kalibrierung aus Daten.

Die Standardmethode ist die exakte Diskretisierung. Der OU-Prozess lässt sich an Beobachtungszeitpunkten als AR(1)-Modell schreiben:

X_{t+Δt} = X_t · e^{−θΔt} + μ · (1 − e^{−θΔt}) + ε

Eine einfache lineare Regression von X_{t+1} auf X_t liefert Steigung a = e^{−θΔt} und Intercept b = μ(1−a). Daraus folgen direkt μ und θ:

import numpy as np
import pandas as pd
from scipy.stats import linregress

def fit_ou(series, dt=1.0):
    """Kalibriere OU-Parameter aus Zeitreihe."""
    x = series.values
    x_t = x[:-1]
    x_tp1 = x[1:]

    slope, intercept, r, p, se = linregress(x_t, x_tp1)
    if slope <= 0 or slope >= 1:
        return None  # kein gültiger OU

    theta = -np.log(slope) / dt
    mu = intercept / (1 - slope)
    # Residuen-Varianz
    resid = x_tp1 - (slope * x_t + intercept)
    sigma2 = np.var(resid) * 2 * theta / (1 - slope**2)
    sigma = np.sqrt(sigma2)

    half_life = np.log(2) / theta
    return {
        'mu': mu,
        'theta': theta,
        'sigma': sigma,
        'half_life': half_life,
        'r_squared': r**2
    }

Die Maximum-Likelihood-Variante liefert nahezu identische Werte, ist aber numerisch stabiler bei sehr kurzen Half-Lives oder hochfrequenten Daten. Für tägliche Bars reicht die OLS-Variante in 99 % der Fälle.

Optimale Trading-Schwellen — analytisch.

Im Gegensatz zu reinem Z-Score-Trading lässt sich für OU-Prozesse die optimale Entry/Exit-Schwelle analytisch herleiten. Bertram (2009) und nachfolgende Arbeiten zeigen: Wenn Sie pro Trade einen festen Kostenanteil c haben und Long/ Short-symmetrisch handeln, maximieren Sie den erwarteten Gewinn pro Zeiteinheit über eine Entry-Schwelle a und Exit auf Mittelwert.

Das Optimum hängt von der Sharpe-pro-Trade a / σ_stat und der erwarteten Tradezeit ab. Für typische Kosten von 5–10 bps pro Trade-Seite und Half-Lives um 15 Tage ergeben sich Entry-Schwellen zwischen 1,2 und 1,8 Standardabweichungen — und interessanterweise oft nicht symmetrisch (asymmetrische Kosten, asymmetrische Drift- Risiken).

In der Praxis lohnt sich eine Grid-Search über mögliche Schwellen, weil das analytische Optimum unter idealen Annahmen gilt, die Realität aber regelmäßig verletzt sind (Kosten variabel, Fills nicht garantiert, Strategie nur Quasi-OU).

Multi-dimensionale OU: Faktor-Spreads gleichzeitig handeln.

Wenn Sie nicht einen Spread haben, sondern 50, lohnt sich eine multivariate OU-Sicht. Der Vektor-Prozess folgt:

dX_t = Θ · (μ − X_t) · dt + Σ^{1/2} · dW_t

Θ ist jetzt eine Matrix, die nicht nur Eigen-Reversion, sondern auch Cross-Reversion modelliert: Wenn Spread A heute zu weit oben ist, kann sich das auch auf die Geschwindigkeit von Spread B auswirken. Für korrelierte Pair-Cluster (etwa innerhalb eines Sektors) bringt das oft 10–20 % zusätzliche Performance.

def fit_multivariate_ou(X, dt=1.0):
    """X: DataFrame mit Spreads in Spalten. Vector-OU via VAR(1)."""
    X_t = X.iloc[:-1].values
    X_tp1 = X.iloc[1:].values
    # OLS pro Spalte (oder geschlossene Matrix-Form)
    A, _, _, _ = np.linalg.lstsq(
        np.hstack([X_t, np.ones((len(X_t), 1))]),
        X_tp1, rcond=None
    )
    # A ist (k+1, k); Steigungs-Block ist obere k x k
    k = X.shape[1]
    slope_matrix = A[:k, :]
    intercept = A[k, :]
    # Theta-Matrix aus Matrix-Logarithmus
    from scipy.linalg import logm
    Theta = -logm(slope_matrix) / dt
    # Equilibrium-Mittelwert
    mu = np.linalg.solve(np.eye(k) - slope_matrix, intercept)
    # Residuen-Kovarianz
    resid = X_tp1 - X_t @ slope_matrix - intercept
    Sigma = np.cov(resid.T)
    return Theta, mu, Sigma

Wann der OU-Annahme nicht mehr passt.

OU ist ein lineares, gauss'sches Modell mit konstanten Parametern. Die Realität ist keines davon. Vier typische Verletzungen:

Pragmatisch: Wenn der Spread auf dem Q-Q-Plot grob normal aussieht und Half-Life stabil bleibt, ist OU ein vernünftiges Modell. Wenn nicht, wechseln Sie auf nicht- parametrische Schwellen oder ein robustes Z-Score-Setup.

Was wir aus OU für jede Strategie mitnehmen.

Auch wenn die Strategie am Ende keine analytischen OU-Schwellen nutzt, ist die OU-Kalibrierung ein hervorragendes Diagnose-Werkzeug. Sie sagt Ihnen:

  1. Wie schnell der Spread mean-revertet (Half-Life) — entscheidend für Trade-Frequenz und Kostenrelevanz.
  2. Wie volatil der Spread im stationären Zustand ist — entscheidend für Z-Score-Skalen.
  3. Wie weit der aktuelle Wert vom Mittelwert entfernt ist — entscheidend für Entry-Timing.

Bei uns läuft jedes neue Stat-Arb-Setup zuerst durch eine OU-Kalibrierung. Pairs mit instabilen θ-Schätzern oder R² unter 0,05 werden gar nicht erst weiterverfolgt — egal wie schön der Z-Score-Chart aussieht. Diese Vorab-Filterung allein hat uns über die Jahre eine zweistellige Anzahl Fehl-Strategien erspart.

Sie wollen ein Mean-Reversion-Setup auf OU-Basis aufbauen oder ein bestehendes Modell stresstesten? Erstgespräch buchen — wir machen die Kalibrierung, die Schwellen-Optimierung und die Live-Implementierung.