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

Koopman Operators and DMD: Do Market Modes Survive Out-of-Sample?

Koopman Operators and DMD: Do Market Modes Survive Out-of-Sample?
#mathematics
#Koopman
#dynamical-systems
#spectral
#prediction

Durch die dynamische Modenzerlegung erhalten Sie ein Spektrum: eine Handvoll komplexer Eigenwerte mit jeweils einer Wachstumsrate und einer Frequenz, die jeweils einem räumlichen Modus über Ihrem Asset-Querschnitt zugeordnet sind. Es sieht aus wie eine Struktur. Die ganze Frage ist, ob es eine Struktur ist oder ob es sich um einen linearen Operator handelt, der sich pflichtbewusst ein Rauschfenster merkt.

Diese Frage hat zwei überprüfbare Hälften, und dieser Artikel ist um sie herum aufgebaut:

  1. Modus-Persistenz. Passt DMD in das Fenster ein tt und am Fenster t+1t+1. Überspannen die vorherrschenden Modi denselben Unterraum oder werden sie bei jeder Umrüstung neu gemischt? Wenn sie neu mischen, ist DMD eine In-Sample-Zerlegung und nichts weiter – und das zu sagen ist eindeutig nützlicher als ein weiteres Tutorial.
  2. Spektralradius als Frühindikator. Der größte Eigenwertmodul ρ=maxjλj\rho = \max_j |\lambda_j| ist ein einzelner Skalar, der zusammenfasst, wie explosiv die angepasste Dynamik ist. Führt es zur realisierten Volatilität, verzögert es sie oder wiederholt es sie lediglich? Jede Antwort kann veröffentlicht werden. nur das erste ist handelbar.

Dass Märkte instationäre nichtlineare Systeme sind, wird hier angenommen und nicht argumentiert – der Blog hat dies bereits argumentiert, mit Phasenraumgeometrie in Attraktoren im Algotrading und mit gemessenen BTC-Statistiken pro Regime in Regimeerkennung mit HMMs. Was folgt, ist die engere Frage, was Nichtstationarität mit einem angepassten Koopman-Operator macht und wie man sie misst.

1. Die Kernidee: Linearisierung nichtlinearer Dynamik

Nichtlineare Marktbewegung wird zu linearem latentem Fluss

Betrachten Sie ein zeitdiskretes dynamisches System auf einem Zustandsraum MRn\mathcal{M} \subseteq \mathbb{R}^n:

xk+1=F(xk)x_{k+1} = F(x_k)

Wo F:MMF: \mathcal{M} \to \mathcal{M} ist eine möglicherweise nichtlineare Karte. Für Märkte, xkx_k ist ein Vektor für Vermögensrenditen, Volatilitäten oder Ungleichgewichte im Auftragsbuch zu einem bestimmten Zeitpunkt kk.

Der Koopman-Operator K\mathcal{K} handelt nicht auf den Staat xx direkt, aber auf skalarwertigen beobachtbaren Funktionen g:MCg: \mathcal{M} \to \mathbb{C}:

[Kg](x)=g(F(x))[\mathcal{K} g](x) = g(F(x))

Die Schlüsseleigenschaft: K\mathcal{K} ist linear, auch wenn FF ist nicht. Der Preis ist Dimensionalität – K\mathcal{K} wirkt auf einen unendlichdimensionalen Funktionenraum. Eine gute endlichdimensionale Näherung verschafft Ihnen die Ausdruckskraft der nichtlinearen Dynamik mit der Nachvollziehbarkeit der linearen Algebra: Vorhersage wird zur Matrixpotenzierung, und jeder Modus ist überprüfbar und nicht in Netzwerkgewichten vergraben.

2. Spektrale Zerlegung des Koopman-Operators

Kohärente Spektralmodi der Marktdynamik

Wenn K\mathcal{K} hat Eigenwerte λj\lambda_j und Eigenfunktionen φj\varphi_j, Dann Kφj=λjφj\mathcal{K} \varphi_j = \lambda_j \varphi_jund jede beobachtbare Größe gg im Bereich dieser Eigenfunktionen zerfällt als:

g(xk)=j=1φj(x0)λjkvjg(x_k) = \sum_{j=1}^{\infty} \varphi_j(x_0) \, \lambda_j^{k} \, v_j

Wo vjv_j sind die Koopman-Moden – vektorwertige Koeffizienten, die beschreiben, wie jede Eigenfunktion zum vollständigen beobachtbaren Vektor beiträgt.

Jeder Eigenwert λj=λjeiωj\lambda_j = |\lambda_j| e^{i\omega_j} kodiert eine Wachstums- oder Verfallsrate (λj|\lambda_j|) und eine Schwingungsfrequenz (ωj\omega_j):

Komponente Eigenwert-Eigenschaft Finanzinterpretation
Trend λ1\lambda \approx 1, ω0\omega \approx 0 Langsame Drift, Schwung
Zyklen λ1\lvert\lambda\rvert \approx 1, ω0\omega \neq 0 Schwankungen, Saisonalität
Transienten λ<1\lvert\lambda\rvert < 1 Abklingende Schocks, kurzlebige Bewegungen
Instabile Modi λ>1\lvert\lambda\rvert > 1 Wachsende, explosive Dynamik

Dieser Tisch ist das Versprechen. In Abschnitt 4 wird das Versprechen anhand der Daten überprüft.

3. Dynamic Mode Decomposition (DMD)

Dynamische Modi zerlegt und neu zusammengesetzt

DMD ist der Arbeitsalgorithmus zur Approximation K\mathcal{K} aus Daten. Gegebene Schnappschüsse, angeordnet in Matrizen:

X=[x0x1xm1],X=[x1x2xm]X = \begin{bmatrix} x_0 & x_1 & \cdots & x_{m-1} \end{bmatrix}, \quad X' = \begin{bmatrix} x_1 & x_2 & \cdots & x_m \end{bmatrix}

DMD sucht nach einem am besten geeigneten linearen Operator AA mit XAXX' \approx AX:

  1. Berechnen Sie die SVD: X=UΣVX = U \Sigma V^*
  2. Projekt: A~=UXVΣ1\tilde{A} = U^* X' V \Sigma^{-1}
  3. Eigenzerlegung: A~W=WΛ\tilde{A} W = W \Lambda
  4. Vollraummodi wiederherstellen: Φ=XVΣ1W\Phi = X' V \Sigma^{-1} W

Die Spalten von Φ\Phi sind die DMD-Modi; die Diagonale von Λ\Lambda enthält die DMD-Eigenwerte.

import numpy as np
from numpy.linalg import svd, eig, lstsq

def dmd(X: np.ndarray, rank: int | None = None) -> tuple:
    """
    Dynamic Mode Decomposition.

    Parameters
    ----------
    X : np.ndarray, shape (n_features, n_snapshots)
        Data matrix where each column is a state snapshot.
    rank : int or None
        Truncation rank for the SVD. None = no truncation.

    Returns
    -------
    eigenvalues : np.ndarray, shape (r,)
        DMD eigenvalues (approximating Koopman eigenvalues).
    modes : np.ndarray, shape (n_features, r)
        DMD modes (columns), L2-normalised.
    amplitudes : np.ndarray, shape (r,)
        Mode amplitudes fitted to the FINAL snapshot, so that a one-step
        forecast is simply modes @ (eigenvalues * amplitudes).
    """
    X0 = X[:, :-1]
    X1 = X[:, 1:]

    U, S, Vh = svd(X0, full_matrices=False)

    if rank is not None:
        U = U[:, :rank]
        S = S[:rank]
        Vh = Vh[:rank, :]

    S_inv = np.diag(1.0 / S)

    A_tilde = U.conj().T @ X1 @ Vh.conj().T @ S_inv
    eigenvalues, W = eig(A_tilde)

    modes = X1 @ Vh.conj().T @ S_inv @ W

    norms = np.linalg.norm(modes, axis=0)
    norms[norms == 0] = 1.0
    modes = modes / norms

    amplitudes = lstsq(modes, X[:, -1].astype(complex), rcond=None)[0]

    return eigenvalues, modes, amplitudes

Zwei bewusste Entscheidungen hier. Modi sind L2-normalisiert, da in Abschnitt 4.2 Modusunterräume über Fenster hinweg verglichen werden und nicht normalisierte Amplituden den Vergleich überfordern würden. Amplituden werden an den letzten Schnappschuss und nicht an den ersten angepasst, was bedeutet, dass eine Prognose einen Eigenwert niemals stark potenziert – die Ursache für die numerische Vergrößerung, die naive DMD-Signale wie Dynamiken aussehen lässt, wenn es sich um Gleitkommaüberläufe handelt.

4. Die Messung

Beobachtungen in Zustandsraumstruktur umgewandelt

Dies ist der Teil des Artikels, der kein Lehrbuch ist. Alles oben Genannte ist ein passendes Verfahren; Unten finden Sie das Protokoll, um herauszufinden, ob die Passform etwas bedeutet.

Daten. Verwenden Sie die projekteigenen Börsendaten – einen Querschnitt von BTC, ETH und liquiden Alts in einem konsistenten Minuten- oder Tick-Raster, kein täglicher Download von Aktien-ETFs. Der Blog ist kryptoorientiert und das Mikrostrukturargument lässt sich nicht übertragen. Bauen XX mit Assets in Zeilen und Zeit in Spalten, bei Protokollrückgaben, pro Asset in jedem Fenster verringert.

4.1 Das Eigenwertspektrum

Bericht, für ein repräsentatives Fenster: wie viele davon rr Liegen Eigenwerte innerhalb einer Toleranz des Einheitskreises, welcher Schwingungsperiode jeweils in Stunden (Periode) entspricht =2π/ωj= 2\pi / |\omega_j| Balken, umgerechnet) und welcher Bruchteil der Renditevarianz die Spitze ist rr Modi rekonstruieren.

def spectrum_report(eigenvalues: np.ndarray, bar_minutes: float,
                    tol: float = 0.05) -> list[dict]:
    """
    Turn a DMD spectrum into human-readable rows: modulus, period in hours,
    and whether the eigenvalue sits on the unit circle within `tol`.
    """
    rows = []
    for lam in eigenvalues:
        modulus = float(np.abs(lam))
        omega = float(np.angle(lam))
        period_hours = (2 * np.pi / abs(omega)) * bar_minutes / 60 if omega else np.inf
        rows.append({
            "modulus": modulus,
            "period_hours": period_hours,
            "on_unit_circle": abs(modulus - 1.0) < tol,
            "regime": "unstable" if modulus > 1 + tol
                      else "persistent" if abs(modulus - 1.0) <= tol
                      else "decaying",
        })
    return sorted(rows, key=lambda r: -r["modulus"])


def reconstruction_r2(X: np.ndarray, modes: np.ndarray,
                      eigenvalues: np.ndarray, amplitudes: np.ndarray) -> float:
    """Fraction of in-window return variance captured by the truncated modes."""
    n_steps = X.shape[1]
    powers = eigenvalues[:, None] ** np.arange(-(n_steps - 1), 1)
    X_hat = (modes @ (amplitudes[:, None] * powers)).real
    resid = np.var(X - X_hat)
    return 1.0 - resid / np.var(X)

Ein ehrlicher Spektrumbericht ist bereits einen Artikel wert. Wenn nichts in der Nähe des Einheitskreises liegt, gibt es keine anhaltenden Handelszyklen und die Geschichte der „jährlichen Saisonalität“ aus der Aktienliteratur lässt sich einfach nicht auf 24/7-Krypto übertragen.

4.2 Modusstabilität über benachbarte Fenster hinweg

Der entscheidende Test. Bringen Sie DMD am Fenster an tt, dann am Fenster t+1t+1und messen, wie viel vom Unterraum der dominanten Mode überlebt. Die richtige Statistik ist keine naive Korrelation von Modenvektoren – Modenreihenfolge und komplexe Phase sind willkürlich –, sondern die Hauptwinkel zwischen den beiden Unterräumen.

def subspace_stability(modes_a: np.ndarray, modes_b: np.ndarray,
                       k: int = 3) -> float:
    """
    Overlap between the leading-k DMD mode subspaces of two adjacent windows.

    Returns the mean cosine of the principal angles: 1.0 = identical subspace,
    0.0 = orthogonal. Immune to mode reordering and complex phase, both of
    which are arbitrary in a DMD fit.
    """
    Qa, _ = np.linalg.qr(modes_a[:, :k])
    Qb, _ = np.linalg.qr(modes_b[:, :k])
    sing = np.linalg.svd(Qa.conj().T @ Qb, compute_uv=False)
    return float(np.mean(np.clip(sing, 0.0, 1.0)))


def stability_curve(returns: np.ndarray, window: int, step: int,
                    rank: int, k: int = 3) -> np.ndarray:
    """Subspace overlap between every pair of adjacent windows."""
    fits = []
    for t_end in range(window, returns.shape[1], step):
        evals, modes, _ = dmd(returns[:, t_end - window:t_end], rank=rank)
        order = np.argsort(-np.abs(evals))
        fits.append(modes[:, order])
    return np.array([subspace_stability(fits[i], fits[i + 1], k=k)
                     for i in range(len(fits) - 1)])

Melden Sie die Verteilung dieser Überlappung und melden Sie sie gegen eine Null: Dieselbe Statistik, berechnet für phasenrandomisierte Surrogate derselben Renditen. Eine hohe Überlappung, die jedoch nicht höher als der Ersatznullwert ist, bedeutet, dass die Modi die Kovarianzstruktur und nicht die Dynamik verfolgen.

4.3 Rollender Spektralradius gegen realisierte Volatilität

Der Anspruch zu testen: ρt=maxjλj\rho_t = \max_j |\lambda_j|, neu berechnet in rollierenden Fenstern, bewegt sich vor der realisierten Volatilität und nicht mit ihr. Der Skalar ist mechanisch neu – der Blog hat bereits zuvor geometrische Skalare in Echtzeit überwacht, insbesondere die Kobayashi-Krümmung in komplexen Mannigfaltigkeiten für den algorithmischen Handel – aber ein Koopman-Eigenwertmodul ist eine andere Größe mit einem anderen Fehlermodus und verdient einen eigenen Lead-Lag-Test statt einer vererbten Steigung.

def rolling_spectral_radius(returns: np.ndarray, window: int = 1440,
                            step: int = 60, rank: int = 5) -> dict:
    """
    Rolling DMD spectrum for regime monitoring.

    returns : np.ndarray, shape (n_assets, n_timesteps)
    window  : rolling window length in bars
    step    : bars between refits
    """
    idx, radii, dom_freq = [], [], []

    for t_end in range(window, returns.shape[1], step):
        X_win = returns[:, t_end - window:t_end]
        try:
            evals, _, _ = dmd(X_win, rank=rank)
        except np.linalg.LinAlgError:
            continue

        idx.append(t_end)
        radii.append(float(np.max(np.abs(evals))))

        on_circle = np.abs(np.abs(evals) - 1.0) < 0.1
        if on_circle.any():
            sel = evals[on_circle]
            dom_freq.append(float(np.abs(np.angle(sel[np.argmax(np.abs(sel))])) / (2 * np.pi)))
        else:
            dom_freq.append(0.0)

    return {"index": np.array(idx),
            "spectral_radius": np.array(radii),
            "dominant_frequency": np.array(dom_freq)}


def lead_lag(signal: np.ndarray, target: np.ndarray, max_lag: int = 24) -> dict:
    """
    Cross-correlation of `signal` against `target` over +/- max_lag steps.
    A peak at negative lag means the signal LEADS the target.
    """
    s = (signal - signal.mean()) / (signal.std() + 1e-12)
    y = (target - target.mean()) / (target.std() + 1e-12)
    lags = np.arange(-max_lag, max_lag + 1)
    corrs = []
    for L in lags:
        if L < 0:
            corrs.append(float(np.corrcoef(s[:L], y[-L:])[0, 1]))
        elif L > 0:
            corrs.append(float(np.corrcoef(s[L:], y[:-L])[0, 1]))
        else:
            corrs.append(float(np.corrcoef(s, y)[0, 1]))
    corrs = np.array(corrs)
    return {"lags": lags, "corr": corrs, "peak_lag": int(lags[np.argmax(np.abs(corrs))])}

Ausrichten spectral_radius zur realisierten Volatilität, die auf demselben Raster berechnet und abgelesen wird peak_lag. Ein Peak bei Lag 0 bedeutet ρt\rho_t ist eine Volatilitätsanpassung mit zusätzlichen Schritten. Ein Peak bei negativer Verzögerung, der über die gesamte Stichprobe und über die Ränge hinweg stabil ist, ist die einzige Version dieses Artikels, die eine handelbare Behauptung enthält.

Ein Hinweis darauf, was nach einem positiven Ergebnis kommt

Wenn ρt\rho_t führt, ist der offensichtliche nächste Schritt die Querschnittskonstruktion: Ordnen Sie die Vermögenswerte nach der DMD-vorhergesagten nächsten Rendite, gehen Sie bei den vorhergesagten Gewinnern long und bei den prognostizierten Verlierern short. Diese Konstruktion ist hier nicht neu – es handelt sich um den Faktor-Residuen-Handel, der bereits in statistische Arbitrage und Paarhandel mit Krypto und Abschnitt 4 von komplexe Arbitrage mit Vektoren und Matrizen behandelt wird; Die einzige wirklich Koopman-spezifische Wendung besteht darin, dass die Eigenportfolios zeitlich veränderliche Eigenwerte und keine statischen PCA-Ladungen tragen.

Der Strategieabschnitt wurde in diesem Artikel bewusst weggelassen, da er hier nicht mit Gebühren und Slippage rückgetestet wurde. Wenn dies der Fall ist, muss es die eigene Messlatte des Blogs überwinden: den Signifikanztest in dem Deflated Sharpe Ratio und mehreren Tests im Vergleich zum ständigen Gegenbeispiel von dem ehrlichen Negativ. Ein DMD-Spektrumplot ist kein Ergebnis.

Für die einstufige Vorhersage selbst sagt die richtige Implementierung anhand des letzten Schnappschusses voraus, anstatt einen Eigenwert hoch zur Fensterlänge zu propagieren:

def dmd_one_step(returns: np.ndarray, rank: int = 4) -> np.ndarray:
    """
    One-step-ahead prediction from the final snapshot of the window.

    Never raise eigenvalues to the window length: any |lambda| != 1 then
    overflows or underflows and the "signal" becomes numerical garbage.
    """
    evals, modes, amplitudes = dmd(returns, rank=rank)
    return (modes @ (evals * amplitudes)).real

5. Erweitertes DMD (EDMD): Nichtlineare Observablen

Nichtlineares Merkmalsfeld zur Erweiterung dynamischer Modi

Standard-DMD arbeitet mit dem Rohzustandsvektor. EDMD hebt die Daten zunächst durch ein Wörterbuch nichtlinearer Basisfunktionen.

Gegeben ein Wörterbuch D={d1,,dp}\mathbf{D} = \{d_1, \ldots, d_p\} von Skalarfunktionen di:RnRd_i: \mathbb{R}^n \to \mathbb{R}, definieren Sie den angehobenen Zustand:

zk=[d1(xk)d2(xk)dp(xk)]Rpz_k = \begin{bmatrix} d_1(x_k) \\ d_2(x_k) \\ \vdots \\ d_p(x_k) \end{bmatrix} \in \mathbb{R}^p

EDMD sucht KRp×pK \in \mathbb{R}^{p \times p} mit zk+1Kzkz_{k+1} \approx K z_k. In der vom folgenden Code verwendeten Konvention wird Folgendes angegeben: Spalten, KK links wirkend:

K=AG1,G=k=0m1zkzkT,A=k=0m1zk+1zkTK = A G^{-1}, \quad G = \sum_{k=0}^{m-1} z_k z_k^{T}, \quad A = \sum_{k=0}^{m-1} z_{k+1} z_k^{T}

Wörterbuchtyp Funktionen Aufnahmen
Polynom xi, xixj, xi2,x_i,\ x_i x_j,\ x_i^2, \ldots Nichtlineare Interaktionen zwischen Assets
Radiale Basis (RBF) exp(γxck2)\exp(-\gamma \lVert x - c_k \rVert^2) Lokale Ähnlichkeit, Regimeclusterung
Zeitverzögerte Einbettung xk, xk1,, xkτx_k,\ x_{k-1}, \ldots,\ x_{k-\tau} Gedächtnis / autoregressive Struktur
Fourier sin(2πfjt), cos(2πfjt)\sin(2\pi f_j t),\ \cos(2\pi f_j t) Bekannte Periodizitäten (Intraday, wöchentlich)
Volatilitätsmerkmale rt, rt2\lvert r_t \rvert,\ r_t^2 Heteroskedastizität, Volumenclusterung

Das Wörterbuch ist der Ort, an dem Domänenwissen Eingang findet, und die Zeitverzögerungszeile ist der Koopman-spezifische Grund, sich überhaupt um Verzögerungskoordinaten zu kümmern: Es handelt sich nicht um eine separate Technik, die angeschraubt wird, sondern um einen weiteren Block der Lifting-Karte. Die Einbettung selbst – Wahl der Verzögerung τ\tau, Einbettungsdimension ddund der Rekonstruktionssatz dahinter – sind bereits eingeführt und kodiert in komplexe Mannigfaltigkeiten für den algorithmischen Handel; Nehmen Sie die Verzögerungsvektoren von dort und speisen Sie sie direkt ein build_financial_dictionary als zusätzliche Zeilen.

import numpy as np
from itertools import combinations_with_replacement

def build_financial_dictionary(X: np.ndarray, max_poly_degree: int = 2,
                               include_volatility: bool = True,
                               delay_steps: int = 0) -> np.ndarray:
    """
    Build a dictionary of nonlinear observables for EDMD.

    X : np.ndarray, shape (n_features, n_snapshots)
    Returns Z of shape (n_dict, n_snapshots - delay_steps).
    """
    n_features, n_snapshots = X.shape
    offset = max(delay_steps, 0)
    X_eff = X[:, offset:]
    n_eff = X_eff.shape[1]

    lifted = [X_eff]  # degree-1 terms (identity)

    if max_poly_degree >= 2:
        for deg in range(2, max_poly_degree + 1):
            for combo in combinations_with_replacement(range(n_features), deg):
                term = np.ones(n_eff)
                for idx in combo:
                    term *= X_eff[idx]
                lifted.append(term.reshape(1, -1))

    if include_volatility:
        lifted.append(np.abs(X_eff))   # absolute returns
        lifted.append(X_eff ** 2)      # squared returns

    for d in range(1, delay_steps + 1):
        lifted.append(X[:, offset - d : n_snapshots - d])

    return np.vstack(lifted)


def edmd(X: np.ndarray, dictionary_fn=None, reg: float = 1e-8,
         **dict_kwargs) -> tuple:
    """
    Extended Dynamic Mode Decomposition.

    Solves Z1 ~= K @ Z0 in the least-squares sense. Uses a least-squares
    solve rather than an explicit Gram inverse: `inv` on a near-singular
    dictionary Gram matrix is how EDMD spectra get silently corrupted.
    """
    if dictionary_fn is None:
        dictionary_fn = lambda x: build_financial_dictionary(x, **dict_kwargs)

    Z = dictionary_fn(X)
    Z0, Z1 = Z[:, :-1], Z[:, 1:]

    p = Z0.shape[0]
    G = Z0 @ Z0.T + reg * np.eye(p)   # regularised Gram matrix
    A = Z1 @ Z0.T

    K = np.linalg.solve(G, A.T).T

    eigenvalues, eigenvectors = np.linalg.eig(K)
    return K, eigenvalues, eigenvectors

6. Deep Koopman Networks

Deep Network Learning latente lineare Dynamik

Das EDMD-Wörterbuch ist handgefertigt, was eine echte Einschränkung darstellt, wenn der Koopman-invariante Unterraum unbekannt ist. Tiefe Koopman-Netzwerke lernen das Heben und den Bediener gemeinsam.

Die Architektur ist ein Autoencoder – Encoder, latenter Code, Decoder, Rekonstruktionsverlust, alles wie in Anomalieerkennung im Algotrading vorgestellt – mit einer Ergänzung, die den ganzen Sinn des Abschnitts ausmacht: ein Linearitätsverlust, der die latente Dynamik durch eine einzige gelernte Matrix zwingt KK.

x_k  -->  [Encoder φ]  -->  z_k  -->  [Linear K]  -->  z_{k+1}  -->  [Decoder ψ]  -->  x̂_{k+1}
                              |                            |
                              +--- Linearity loss: ‖z_{k+1} - K z_k‖ ---+

L=xk+1ψ(Kφ(xk))2prediction+αφ(xk+1)Kφ(xk)2linearity+βxkψ(φ(xk))2reconstruction\mathcal{L} = \underbrace{\lVert x_{k+1} - \psi(K \varphi(x_k)) \rVert^2}_{\text{prediction}} + \alpha \underbrace{\lVert \varphi(x_{k+1}) - K \varphi(x_k) \rVert^2}_{\text{linearity}} + \beta \underbrace{\lVert x_k - \psi(\varphi(x_k)) \rVert^2}_{\text{reconstruction}}

Ohne die α\alpha Mit anderen Worten, Sie haben einen gewöhnlichen Autoencoder, auf dessen latenten Raum zufällig eine Matrixmultiplikation folgt. Damit wird das Netzwerk für jede latente Darstellung bestraft, deren Entwicklung nicht linear ist – was das Gelernte ausmacht KK eine Koopman-Näherung und ihre Eigenwerte vergleichbar mit dem DMD-Spektrum von Abschnitt 4.

import torch
import torch.nn as nn

class DeepKoopman(nn.Module):
    """Deep Koopman autoencoder: learned lifting + linear latent dynamics."""

    def __init__(self, input_dim: int, latent_dim: int, hidden_dim: int = 128):
        super().__init__()

        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim), nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim), nn.ReLU(),
            nn.Linear(hidden_dim, latent_dim),
        )
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, hidden_dim), nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim), nn.ReLU(),
            nn.Linear(hidden_dim, input_dim),
        )
        self.K = nn.Linear(latent_dim, latent_dim, bias=False)

    def encode(self, x: torch.Tensor) -> torch.Tensor:
        return self.encoder(x)

    def decode(self, z: torch.Tensor) -> torch.Tensor:
        return self.decoder(z)

    def forward(self, x_k: torch.Tensor) -> dict:
        z_k = self.encode(x_k)
        z_k1_pred = self.K(z_k)
        return {
            "z_k": z_k,
            "z_k1_pred": z_k1_pred,
            "x_k1_pred": self.decode(z_k1_pred),
            "x_k_recon": self.decode(z_k),
        }

    def multi_step_predict(self, x_0: torch.Tensor, n_steps: int) -> torch.Tensor:
        """Roll out by repeated application of the linear operator."""
        z = self.encode(x_0)
        preds = []
        for _ in range(n_steps):
            z = self.K(z)
            preds.append(self.decode(z))
        return torch.stack(preds, dim=1)

    def latent_spectrum(self) -> np.ndarray:
        """Eigenvalues of the learned K — directly comparable to DMD's."""
        return np.linalg.eigvals(self.K.weight.detach().cpu().numpy())


def koopman_loss(model: DeepKoopman, x_k: torch.Tensor, x_k1: torch.Tensor,
                 alpha: float = 1.0, beta: float = 0.5) -> torch.Tensor:
    out = model(x_k)
    z_k1_true = model.encode(x_k1)

    prediction = nn.functional.mse_loss(out["x_k1_pred"], x_k1)
    linearity = nn.functional.mse_loss(out["z_k1_pred"], z_k1_true)
    reconstruction = nn.functional.mse_loss(out["x_k_recon"], x_k)

    return prediction + alpha * linearity + beta * reconstruction

latent_spectrum Deshalb lohnt es sich, dieses Training zu absolvieren, anstatt nach einem Sequenzmodell zu greifen: Der erlernte Operator ist immer noch eine Matrix, daher gelten der Stabilitätstest in Abschnitt 4.2 und der Lead-Lag-Test in Abschnitt 4.3 unverändert für ein tiefes Modell.

7. Praktische Überlegungen und Fallstricke

Navigieren durch Modelldrift und spärliche Beobachtungen

Rangauswahl. Der Kürzungsrang rr ist ein Bias-Varianz-Regler – zu niedrig verfehlt die Dynamik, zu hoch passt zu Rauschen. Beobachten Sie nicht einen Ellenbogen mit singulärem Wert; Der Blog antwortet bereits richtig mit der Marchenko-Pastur-Begrenzung aus der Zufallsmatrixtheorie in [komplexe Arbitrage mit Vektoren und Matrizen] (/en/blog/post/complex-arbitrage-vector-matrix). Behalten Sie die Komponenten bei, deren Singularwerte die Marchenko-Pastur-Kante für Ihre Fensterform überschreiten, und geben Sie das Ergebnis an rr explizit neben jedem Spektrum, das Sie veröffentlichen.

Fensterlänge. Die Koopman-Theorie geht von einer festen aus FF; Märkte bieten keins an. Rollierende Umrüstungen sind obligatorisch, und die Stabilitätskurve in Abschnitt 4.2 ist genau die Diagnose dafür, ob das gewählte Zeitfenster lang genug für eine Schätzung und kurz genug ist, um innerhalb eines Regimes zu bleiben.

Rauschenempfindlichkeit. Finanzdaten haben ein niedriges Signal-Rausch-Verhältnis und Standard-DMD ist durch Rauschen verzerrt XX. Abhilfemaßnahmen, die es wert sind, ausprobiert zu werden, bevor man zu dem Schluss kommt, dass die Modi instabil sind:

  • Gesamt-DMD (TDMD) – behandelt beides XX Und XX' als verrauscht über die Gesamtzahl der kleinsten Quadrate.
  • Optimiertes DMD – optimiert direkt die Eigenwertmoduszerlegung anhand der restlichen Frobenius-Norm.
  • Kernel EDMD – arbeitet implizit in einem hochdimensionalen Merkmalsraum, ohne das Wörterbuch zu erstellen.

Wenn die Modenstabilität unter TDMD wesentlich ansteigt, handelte es sich bei der Instabilität um Messrauschen. Wenn nicht, war es der Markt.

8. Wo DMD sitzt

Dynamische Moduszerlegung zwischen Modellierungsansätzen

Methode Linearität Interpretierbar Mehrstufige Prognose
DMD Linear im Zustandsraum Ja (Moden + Eigenwerte) Stabil (Matrixleistung)
EDMD Linear im angehobenen Raum Ja, angesichts des Wörterbuchs Stabil (Matrixleistung)
Deep Koopman Linear im erlernten Raum Mäßig (latente K prüfen) Stabil (Matrixleistung)

Für volatilitätsspezifische Prognosen ist der Vergleichspunkt die GARCH-Familie – siehe GARCH-Volatilitätsprognose für Krypto. Informationen zu vollständig nichtlinearen Sequenzmodellen und den damit einhergehenden Interpretierbarkeits- und Fehlerakkumulations-Kompromissen finden Sie unter Der Temporal Fusion Transformer im Handel.

Die Nische, die DMD einnimmt, ist eng, aber real: mehrstufige Prognosen, die durch eine einzelne Matrixleistung und nicht durch einen autoregressiven Rollout generiert werden, wobei jeder Modus überprüfbar ist. Ob diese Nische Alpha enthält, ist die Frage von Abschnitt 4, nicht die dieser Tabelle.

Fazit

Turbulente Flugbahnen lösen sich in stabile Modi auf

Die Koopman-Theorie ist eine wirklich elegante Möglichkeit, die Marktdynamik zu betrachten, und genau deshalb erfordert sie den härtesten Test, den es gibt. Die Imbissbuden:

  1. Passen Sie DMD an und testen Sie dann sofort die Anpassung. Die Unterraumüberlappung zwischen benachbarten Fenstern, gemessen an einer phasenrandomisierten Null, sagt Ihnen innerhalb eines Nachmittags, ob Sie eine Struktur gefunden oder sich ein Fenster gemerkt haben.
  2. Der rollende Spektralradius ist der Skalar, den es zu überwachen gilt, und sein Wert hängt vollständig vom Lead-Lag-Ergebnis ab. Bei Lag 0 handelt es sich um einen Volatilitäts-Proxy; Bei negativer Verzögerung handelt es sich um eine Regimewarnung.
  3. Verankern Sie die Amplituden beim letzten Schnappschuss und erhöhen Sie die Eigenwerte niemals auf die Fensterlänge. Ein großer Teil der DMD-„Signale“ in freier Wildbahn sind Gleitkomma-Artefakte.
  4. Wählen Sie den Rang nach Marchenko-Pastur, nicht indem Sie einen Ellbogen beäugen, und veröffentlichen Sie den Rang mit jedem Spektrum.
  5. Ein Spektrumdiagramm ist kein Ergebnis. Jede darauf aufbauende Strategie muss Gebühren, Slippage und einen deflationierten Sharpe-Test überstehen, bevor sie zählt.

Für darüber hinausgehende Implementierungen deckt die PyDMD-Bibliothek die DMD-Varianten umfassend ab, und der Referenzcode von Mallen et al. deckt die tiefe Koopman-Seite ab.

Die Märkte werden chaotisch, instationär und teilweise beobachtet bleiben. Die Koopman-Theorie bietet eine prinzipielle Linse, um aus diesem Chaos Struktur zu extrahieren – vorausgesetzt, Sie überprüfen, ob die Struktur nächste Woche noch vorhanden ist.


Referenzen und weiterführende Literatur:- B. O. Koopman, „Hamiltonian Systems and Transformation in Hilbert Space“, Proceedings of the National Academy of Sciences, 1931.

  • J. H. Tu et al., „On Dynamic Mode Decomposition: Theory and Applications“, Journal of Computational Dynamics, 2014.
  • M. O. Williams, I. G. Kevrekidis, C. W. Rowley, „A Data-Driven Approximation of the Koopman Operator: Extending Dynamic Mode Decomposition“, Journal of Nonlinear Science, 2015.
  • B. Lusch, J. N. Kutz, S. L. Brunton, „Deep Learning for Universal Linear Embeddings of Nonlinear Dynamics“, Nature Communications, 2018.
  • J. Mann und J. N. Kutz, „Dynamic Mode Decomposition for Financial Trading Strategies“, Quantitative Finance, 2016.
  • A. Mallen et al., „Koopman Neural Forecaster for Time Series with Temporal Distribution Shifts“, ICML, 2023.
  • E. Gonzalez und M. Generelo, „Analyse chaotischer Wirtschaftsmodelle durch Koopman-Operatoren, EDMD, Takens-Theorem und maschinelles Lernen“, Data Science in Finance and Economics, 2022.
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.