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

Dynamic Mode Decomposition fits a linear operator to nonlinear market dynamics. The only question that matters: do the fitted modes persist from one window to the next, and does the rolling spectral radius lead realised volatility? Here is the measurement protocol and the code to run it.

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 ρ=max⁡j∣λ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 M⊆Rn\mathcal{M} \subseteq \mathbb{R}^n:

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

waar F:M→MF: \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:M→Cg: \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) λjk vjg(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=∣λj∣eiωj\lambda_j = |\lambda_j| e^{i\omega_j} codeert voor een groei- of vervalsnelheid (∣λj∣|\lambda_j|) en een oscillatiefrequentie (ωj\omega_j):

OnderdeelEigenwaarde eigendomFinanciële interpretatie
Trendλ≈1\lambda \approx 1, ω≈0\omega \approx 0Langzame drift, momentum
Cycli∣λ∣≈1\lvert\lambda\rvert \approx 1, ω≠0\omega \neq 0Schommelingen, seizoensinvloeden
Transiënten∣λ∣<1\lvert\lambda\rvert < 1Rottende schokken, kortstondige bewegingen
Onstabiele modi∣λ∣>1\lvert\lambda\rvert > 1Groeiende, 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=[x0x1⋯xm−1],X′=[x1x2⋯xm]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 X′≈AXX' \approx AX:

  1. Bereken de SVD: X=UΣV∗X = U \Sigma V^*
  2. Project: A~=U∗X′VΣ−1\tilde{A} = U^* X' V \Sigma^{-1}
  3. Eigenontleding: A~W=WΛ\tilde{A} W = W \Lambda
  4. Herstel modi met volledige ruimte: Φ=X′VΣ−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)

    # Reduced operator and its spectrum
    A_tilde = U.conj().T @ X1 @ Vh.conj().T @ S_inv
    eigenvalues, W = eig(A_tilde)

    # Full-space (exact) modes
    modes = X1 @ Vh.conj().T @ S_inv @ W

    # Normalise modes so that mode comparison across windows is scale-free
    norms = np.linalg.norm(modes, axis=0)
    norms[norms == 0] = 1.0
    modes = modes / norms

    # Amplitudes anchored at the last observed snapshot
    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]
    # amplitudes are anchored at the last snapshot -> walk backwards
    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=max⁡j∣λ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:Rn→Rd_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 K∈Rp×pK \in \mathbb{R}^{p \times p} met zk+1≈Kzkz_{k+1} \approx K z_k. In de conventie die door de onderstaande code wordt gebruikt – staat als kolommen, KK links handelend:

K=AG−1,G=∑k=0m−1zkzkT,A=∑k=0m−1zk+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}

WoordenboektypeFunctiesOpnames
Polynoomxi, xixj, xi2,…x_i,\ x_i x_j,\ x_i^2, \ldotsNiet-lineaire interacties tussen activa
Radiale basis (RBF)exp⁡(−γ∥x−ck∥2)\exp(-\gamma \lVert x - c_k \rVert^2)Lokale gelijkenis, clustering van regimes
Inbedding met tijdvertragingxk, xk−1,…, xk−τx_k,\ x_{k-1}, \ldots,\ x_{k-\tau}Geheugen / autoregressieve structuur
Fouriersin⁡(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^2Heteroskedasticiteit, 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 = A @ G^{-1}, obtained as (G^T \ A^T)^T. G is symmetric, so G^T = G.
    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))∥2⏟prediction+α∥φ(xk+1)−Kφ(xk)∥2⏟linearity+β∥xk−ψ(φ(xk))∥2⏟reconstruction\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),
        )
        # Learnable Koopman operator: the ONLY dynamics in latent space
        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 X′X' 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

WerkwijzeLineariteitInterpreteerbaarVoorspelling in meerdere stappen
DMDLineair in toestandsruimteJa (modi + eigenwaarden)Stabiel (matrixvermogen)
EDMDLineair in opgeheven ruimteJa, gezien het woordenboekStabiel (matrixvermogen)
Diepe KoopmanLineair in geleerde ruimteMatig (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.