← Terug naar artikelen
August 12, 2026
5 min leestijd

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

Dynamic Mode Decomposition biedt u een spectrum: een handvol complexe eigenwaarden, elk met een groeisnelheid en een frequentie, elk gekoppeld aan een ruimtelijke modus over de dwarsdoorsnede van uw activa. Het lijkt op structuur. De hele vraag is of het een structuur is of een lineaire operator die plichtsgetrouw één venster met ruis onthoudt.

Die vraag bestaat uit twee testbare helften, en dit artikel is daaromheen gebouwd:

  1. Moduspersistentie. Plaats DMD op venster tt en op raam t+1t+1. Bestrijken de dominante modi dezelfde subruimte, of herschikken ze elke refit? Als ze opnieuw worden geschud, is DMD een ontleding in de steekproef en niets meer - en dat zeggen is duidelijk nuttiger dan een andere tutorial.
  2. Spectrale straal als voorlopende indicator. De grootste eigenwaardemodulus ρ=maxjλj\rho = \max_j |\lambda_j| is een enkele scalaire waarde die samenvat hoe explosief de aangepaste dynamiek is. Leidt het tot de gerealiseerde volatiliteit, blijft het achter of herhaalt het dit alleen maar? Beide antwoorden zijn publiceerbaar; alleen de eerste is verhandelbaar.

Dat markten niet-stationaire niet-lineaire systemen zijn, wordt hier aangenomen, niet beargumenteerd – de blog heeft dit al betoogd, met faseruimtegeometrie in attractors in algotrading en met gemeten BTC-statistieken per regime in regime detectie met HMM’s. Wat volgt is de engere vraag wat niet-stationariteit doet met een gemonteerde Koopman-operator, en hoe deze te meten.

1. Het kernidee: het lineariseren van niet-lineaire dynamieken

Niet-lineaire marktbeweging wordt lineaire latente stroom

Beschouw een discreet-tijd dynamisch systeem op een toestandsruimte MRn\mathcal{M} \subseteq \mathbb{R}^n:

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

waar F:MMF: \mathcal{M} \to \mathcal{M} is een mogelijk niet-lineaire kaart. Voor markten, xkx_k is een vector van activarendementen, volatiliteiten of onevenwichtigheden in het orderboek in de tijdstap kk.

De Koopman-operator K\mathcal{K} handelt niet ten aanzien van de staat xx direct, maar op scalair gewaardeerde waarneembare functies g:MCg: \mathcal{M} \to \mathbb{C}:

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

De belangrijkste eigenschap: K\mathcal{K} is lineair, zelfs wanneer FF niet. De prijs is dimensionaliteit - K\mathcal{K} werkt op een oneindig-dimensionale functieruimte. Met een goede eindig-dimensionale benadering koop je de expressiviteit van niet-lineaire dynamiek met de hanteerbaarheid van lineaire algebra: voorspelling wordt matrixmachtsverheffing, en elke modus is inspecteerbaar in plaats van begraven in netwerkgewichten.

2. Spectrale ontleding van de Koopman-operator

Coherente spectrale vormen van marktdynamiek

Als K\mathcal{K} heeft eigenwaarden λj\lambda_j en eigenfuncties φj\varphi_j, Dan Kφj=λjφj\mathcal{K} \varphi_j = \lambda_j \varphi_jen eventueel waarneembaar gg in de spanwijdte van deze eigenfuncties ontleedt als:

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

waar vjv_j zijn de Koopman-modi: vectorwaardecoëfficiënten die beschrijven hoe elke eigenfunctie bijdraagt ​​aan de volledig waarneembare vector.

Elke eigenwaarde λj=λjeiωj\lambda_j = |\lambda_j| e^{i\omega_j} codeert voor een groei- of vervalsnelheid (λj|\lambda_j|) en een oscillatiefrequentie (ωj\omega_j):

Onderdeel Eigenwaarde eigendom Financiële interpretatie
Trend λ1\lambda \approx 1, ω0\omega \approx 0 Langzame drift, momentum
Cycli λ1\lvert\lambda\rvert \approx 1, ω0\omega \neq 0 Schommelingen, seizoensinvloeden
Transiënten λ<1\lvert\lambda\rvert < 1 Rottende schokken, kortstondige bewegingen
Onstabiele modi λ>1\lvert\lambda\rvert > 1 Groeiende, explosieve dynamiek

Deze tafel is de belofte. In sectie 4 wordt de belofte getoetst aan de gegevens.

3. Ontbinding in dynamische modus (DMD)

Dynamische modi ontleed en opnieuw samengesteld

DMD is het werkpaardalgoritme voor benadering K\mathcal{K} uit gegevens. Gegeven snapshots gerangschikt in matrices:

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 zoekt naar een best passende lineaire operator AA met XAXX' \approx AX:

  1. Bereken de SVD: X=UΣVX = U \Sigma V^*
  2. Project: A~=UXVΣ1\tilde{A} = U^* X' V \Sigma^{-1}
  3. Eigenontleding: A~W=WΛ\tilde{A} W = W \Lambda
  4. Herstel modi met volledige ruimte: Φ=XVΣ1W\Phi = X' V \Sigma^{-1} W

De kolommen van Φ\Phi zijn de DMD-modi; de diagonaal van Λ\Lambda bevat de DMD-eigenwaarden.

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

Twee bewuste keuzes hier. Modi zijn L2-genormaliseerd, omdat sectie 4.2 modussubruimten tussen vensters vergelijkt en niet-genormaliseerde amplitudes de vergelijking zouden ondermijnen. Amplitudes worden aangepast aan de laatste momentopname in plaats van aan de eerste, wat betekent dat een voorspelling een eigenwaarde nooit tot een grote macht verhoogt – de bron van de numerieke opvergroting die ervoor zorgt dat naïeve DMD-signalen op dynamiek lijken als ze een drijvende-komma-overflow zijn.

4. De meting

Waarnemingen getransformeerd in toestandsruimtestructuur

Dit is het deel van het artikel dat geen leerboek is. Alles hierboven is een passende procedure; Hieronder vindt u het protocol om erachter te komen of de pasvorm iets betekent.

Data. Gebruik de eigen uitwisselingsgegevens van het project: een dwarsdoorsnede van BTC, ETH en liquide alts op een consistent minuten- of tick-raster, niet een dagelijkse download van aandelen-ETF's. De blog is crypto-eerst en het microstructuurargument wordt niet overgedragen. Bouwen XX met activa op rijen en tijd op kolommen, op logboekretouren, verlaagd per activa binnen elk venster.

4.1 Het eigenwaardespectrum

Rapporteer, voor een representatief venster: hoeveel van de rr eigenwaarden vallen binnen een tolerantie van de eenheidscirkel, waarbij elke oscillatieperiode overeenkomt met in uren (periode =2π/ωj= 2\pi / |\omega_j| staven, geconverteerd), en welk deel van de rendementsvariantie bovenaan staat rr modi reconstrueren.

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)

Een eerlijk spectrumrapport is het artikel al waard. Als niets in de buurt van de eenheidscirkel komt, zijn er geen aanhoudende cycli om te handelen en het verhaal van de ‘jaarlijkse seizoensinvloeden’ uit de aandelenliteratuur gaat eenvoudigweg niet over op 24/7 crypto.

4.2 Modusstabiliteit in aangrenzende vensters

De beslissende proef. Plaats DMD op het raam tten vervolgens op het raam t+1t+1, en meet hoeveel van de subruimte van de dominante modus overleeft. De juiste statistiek is niet een naïeve correlatie van modusvectoren – modusordening en complexe fase zijn willekeurig – maar de hoofdhoeken tussen de twee deelruimten.

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)])

Rapporteer de verdeling van deze overlap, en rapporteer deze tegen een nul: dezelfde statistiek berekend op basis van fase-gerandomiseerde surrogaten met hetzelfde rendement. Een overlap die hoog is, maar niet hoger dan de surrogaatnul, betekent dat de modi de covariantiestructuur volgen, en niet de dynamiek.

4.3 Rollende spectrale straal tegen gerealiseerde volatiliteit

De claim om te testen: ρt=maxjλj\rho_t = \max_j |\lambda_j|, opnieuw berekend op basis van voortschrijdende vensters, beweegt vóór de gerealiseerde volatiliteit in plaats van ermee. De scalair is mechanisch nieuw – de blog heeft al eerder geometrische scalaires in realtime gevolgd, met name de Kobayashi-kromming in [complexe spruitstukken voor algoritmische handel] (/en/blog/post/complex-spruitstukken-algoritmische-trading) – maar een Koopman-eigenwaardemodulus is een andere grootheid met een andere faalmodus, en verdient zijn eigen lead-lag-test in plaats van een erfelijke toonhoogte.

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))])}

Uitlijnen spectral_radius naar de gerealiseerde volatiliteit, berekend op hetzelfde raster en afgelezen peak_lag. Een piek bij lag 0 betekent ρt\rho_t is een volatiliteitsherformulering met extra stappen. Een piek met negatieve vertraging, stabiel in de hele steekproef en in alle rangen, is de enige versie van dit artikel met een verhandelbare claim erin.

Een opmerking over wat er komt na een positief resultaat

Als ρt\rho_t leidt, is de voor de hand liggende volgende stap de cross-sectionele constructie: activa rangschikken op basis van het door DMD voorspelde volgende staprendement, long gaan bij de voorspelde winnaars en short gaan bij de voorspelde verliezers. Die constructie is hier niet nieuw – het is de factor-resthandel die al wordt behandeld in statistische arbitrage en parenhandel in crypto en sectie 4 van complexe arbitrage met vectoren en matrices; de enige echt Koopman-specifieke wending is dat de eigenportfolio's tijdsvariërende eigenwaarden bevatten in plaats van statische PCA-belastingen.

Het strategiegedeelte is met opzet afwezig in dit artikel, omdat het hier niet is getest met vergoedingen en ontsporingen. Als dat zo is, moet het de eigen balk van de blog leegmaken: de significantietest in de Deflated Sharpe Ratio en meervoudige testen, tegen het staande tegenvoorbeeld van het eerlijke negatieve. Een DMD-spectrumplot is geen resultaat.

Voor de eenstapsvoorspelling zelf voorspelt de juiste implementatie op basis van de laatste momentopname in plaats van een eigenwaarde door te geven aan de macht van de vensterlengte:

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. Uitgebreide DMD (EDMD): niet-lineaire observaties

Niet-lineaire functieveld die dynamische modi uitbreidt

Standaard DMD werkt op de onbewerkte toestandsvector. EDMD tilt de gegevens eerst door een woordenboek van niet-lineaire basisfuncties.

Gegeven een woordenboek D={d1,,dp}\mathbf{D} = \{d_1, \ldots, d_p\} van scalaire functies di:RnRd_i: \mathbb{R}^n \to \mathbb{R}, definieer de opgeheven status:

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 zoekt KRp×pK \in \mathbb{R}^{p \times p} met zk+1Kzkz_{k+1} \approx K z_k. In de conventie die door de onderstaande code wordt gebruikt – staat als kolommen, KK links handelend:

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}

Woordenboektype Functies Opnames
Polynoom xi, xixj, xi2,x_i,\ x_i x_j,\ x_i^2, \ldots Niet-lineaire interacties tussen activa
Radiale basis (RBF) exp(γxck2)\exp(-\gamma \lVert x - c_k \rVert^2) Lokale gelijkenis, clustering van regimes
Inbedding met tijdvertraging xk, xk1,, xkτx_k,\ x_{k-1}, \ldots,\ x_{k-\tau} Geheugen / autoregressieve structuur
Fourier sin(2πfjt), cos(2πfjt)\sin(2\pi f_j t),\ \cos(2\pi f_j t) Bekende periodiciteiten (intraday, wekelijks)
Volatiliteitskenmerken rt, rt2\lvert r_t \rvert,\ r_t^2 Heteroskedasticiteit, vol clustering

Het woordenboek is waar domeinkennis binnenkomt, en de tijdvertragingsrij is de Koopman-specifieke reden om überhaupt aandacht te besteden aan vertragingscoördinaten: ze zijn geen afzonderlijke techniek die eraan vastgeschroefd is, ze zijn nog een blok van de hijskaart. De inbedding zelf – de keuze voor uitstel τ\tau, inbeddingsdimensie dd, en de reconstructiestelling erachter — is al geïntroduceerd en gecodeerd in complexe verdeelstukken voor algoritmische handel; neem de vertragingsvectoren vanaf daar en voer ze er rechtstreeks in build_financial_dictionary als extra rijen.

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. Diepe Koopman-netwerken

Diep netwerk lerende latente lineaire dynamiek

Het EDMD-woordenboek is met de hand vervaardigd, wat een echte beperking is als de Koopman-invariante deelruimte onbekend is. Diepe Koopman-netwerken leren gezamenlijk het hijsen en de operator kennen.

De architectuur is een autoencoder – encoder, latente code, decoder, reconstructieverlies, allemaal zoals geïntroduceerd in anomaliedetectie in algotrading – met één toevoeging die het hele punt van de sectie is: een lineariteitsverlies dat de latente dynamiek door een enkele geleerde matrix dwingt 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}}

Zonder de α\alpha Op termijn heb je een gewone autoencoder waarvan de latente ruimte toevallig wordt gevolgd door een matrixvermenigvuldiging. Hiermee wordt het netwerk gestraft voor elke latente representatie waarvan de evolutie niet lineair is – wat ervoor zorgt dat de geleerde KK een Koopman-benadering en zijn eigenwaarden vergelijkbaar met het DMD-spectrum van sectie 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 Dat maakt dit de moeite waard om te trainen in plaats van te streven naar een sequentiemodel: de geleerde operator is nog steeds een matrix, dus de stabiliteitstest van sectie 4.2 en de lead-lag-test van sectie 4.3 zijn onveranderd van toepassing op een diep model.

7. Praktische overwegingen en valkuilen

Navigerende modeldrift en schaarse observaties

Rankselectie. De truncatierang rr is een bias-variantie-wijzerplaat: te laag mist de dynamiek, te hoog past bij ruis. Kijk niet naar een elleboog met een bijzondere waarde; de blog geeft al het juiste antwoord op "hoeveel componenten voordat je ruis aanpast", waarbij de Marchenko-Pastur is afgeleid van de Random Matrix Theory in [complexe arbitrage met vectoren en matrices] (/en/blog/post/complex-arbitrage-vector-matrix). Bewaar de componenten waarvan de singuliere waarden groter zijn dan de Marchenko-Pastur-rand voor uw raamvorm, en vermeld het resultaat rr expliciet naast elk spectrum dat u publiceert.

Vensterlengte. Koopman-theorie gaat uit van een vaste waarde FF; markten bieden er geen. Rollende refits zijn verplicht, en de stabiliteitscurve van sectie 4.2 is precies de diagnose of het gekozen venster lang genoeg is om te schatten en kort genoeg om binnen één regime te blijven.

Geluidsgevoeligheid. Financiële gegevens hebben een lage signaal-ruisverhouding en standaard DMD is vertekend door ruis in XX. Oplossingen die de moeite waard zijn om te proberen voordat we concluderen dat de modi onstabiel zijn:

  • Totaal DMD (TDMD) — behandelt beide XX En XX' zo luidruchtig via de totale kleinste kwadraten.
  • Geoptimaliseerde DMD — optimaliseert direct de ontleding van de eigenwaardemodus ten opzichte van de resterende Frobenius-norm.
  • Kernel EDMD — werkt impliciet in een hoogdimensionale featureruimte zonder een woordenboek op te bouwen.

Als de modusstabiliteit aanzienlijk stijgt onder TDMD, was de instabiliteit meetruis. Als dat niet het geval is, was het de markt.

8. Waar DMD zit

Dynamische modusontleding tussen modelleringsbenaderingen

Werkwijze Lineariteit Interpreteerbaar Voorspelling in meerdere stappen
DMD Lineair in toestandsruimte Ja (modi + eigenwaarden) Stabiel (matrixvermogen)
EDMD Lineair in opgeheven ruimte Ja, gezien het woordenboek Stabiel (matrixvermogen)
Diepe Koopman Lineair in geleerde ruimte Matig (inspecteer latente K) Stabiel (matrixvermogen)

Voor volatiliteitsspecifieke voorspellingen is het vergelijkingspunt de GARCH-familie – zie GARCH volatility forecasting for crypto. Voor volledig niet-lineaire sequentiemodellen en de wisselwerking tussen interpretatie en foutaccumulatie die zij met zich meebrengen, zie de Temporal Fusion Transformer in trading.

De niche die DMD inneemt is smal maar reëel: meerstapsvoorspellingen gegenereerd door een enkele matrixmacht in plaats van autoregressieve uitrol, waarbij elke modus inspecteerbaar is. Of die niche alfa bevat, is de vraag van sectie 4, niet die van deze tabel.

Conclusie

Turbulente trajecten die overgaan in stabiele modi

De Koopman-theorie is een werkelijk elegante manier om naar de marktdynamiek te kijken, en elegantie is precies de reden waarom deze de zwaarste test moet ondergaan die er is. De afhaalrestaurants:

  1. Fitte DMD, test dan onmiddellijk de pasvorm. Subruimte-overlap tussen aangrenzende vensters, gemeten tegen een fase-gerandomiseerde nul, vertelt u binnen een middag of u structuur hebt gevonden of een venster hebt opgeslagen.
  2. De rollende spectrale straal is de enige scalair die het waard is om te monitoren, en de waarde ervan hangt volledig af van het lead-lag-resultaat. Bij lag 0 is het een volatiliteitsproxy; bij negatieve vertraging is het een waarschuwing van het regime.
  3. Veranker de amplitudes bij de laatste momentopname en verhoog de eigenwaarden nooit naar de vensterlengte. Een groot deel van de DMD-signalen in het wild zijn drijvende-komma-artefacten.
  4. Kies de rang op basis van Marchenko-Pastur, niet door op een elleboog te kijken, en publiceer de rang bij elk spectrum.
  5. Een spectrumplot is geen resultaat. Elke strategie die hierop wordt gebouwd, moet vergoedingen, ontsporingen en een leeggelopen Sharpe-test overleven voordat deze telt.

Voor implementaties die verder gaan, dekt de PyDMD-bibliotheek de DMD-varianten uitgebreid, en de referentiecode van Mallen et al. bestrijkt de diepe Koopman-kant.

De markten zullen rommelig, niet-stationair en gedeeltelijk geobserveerd blijven. De Koopman-theorie biedt een principiële lens om structuur uit die puinhoop te halen – op voorwaarde dat je controleert of de structuur er volgende week nog steeds is.


Referenties en verder lezen:- 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 en J.N. Kutz, "Dynamic Mode Decomposition for Financial Trading Strategies", Quantitative Finance, 2016.
  • A. Mallen et al., "Koopman Neural Forecaster voor tijdreeksen met temporele distributieverschuivingen", ICML, 2023.
  • E. Gonzalez en M. Generelo, "Analyse van chaotische economische modellen via Koopman-operatoren, EDMD, de stelling van Takens en Machine Learning", Data Science in Finance and Economics, 2022.
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.