Koopman Operators and DMD: Do Market Modes Survive Out-of-Sample?
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:
- Moduspersistentie. Plaats DMD op venster en op raam . 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.
- Spectrale straal als voorlopende indicator. De grootste eigenwaardemodulus 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

Beschouw een discreet-tijd dynamisch systeem op een toestandsruimte :
waar is een mogelijk niet-lineaire kaart. Voor markten, is een vector van activarendementen, volatiliteiten of onevenwichtigheden in het orderboek in de tijdstap .
De Koopman-operator handelt niet ten aanzien van de staat direct, maar op scalair gewaardeerde waarneembare functies :
De belangrijkste eigenschap: is lineair, zelfs wanneer niet. De prijs is dimensionaliteit - 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

Als heeft eigenwaarden en eigenfuncties , Dan en eventueel waarneembaar in de spanwijdte van deze eigenfuncties ontleedt als:
waar zijn de Koopman-modi: vectorwaardecoëfficiënten die beschrijven hoe elke eigenfunctie bijdraagt aan de volledig waarneembare vector.
Elke eigenwaarde codeert voor een groei- of vervalsnelheid () en een oscillatiefrequentie ():
| Onderdeel | Eigenwaarde eigendom | Financiële interpretatie |
|---|---|---|
| Trend | , | Langzame drift, momentum |
| Cycli | , | Schommelingen, seizoensinvloeden |
| Transiënten | Rottende schokken, kortstondige bewegingen | |
| Onstabiele modi | Groeiende, explosieve dynamiek |
Deze tafel is de belofte. In sectie 4 wordt de belofte getoetst aan de gegevens.
3. Ontbinding in dynamische modus (DMD)

DMD is het werkpaardalgoritme voor benadering uit gegevens. Gegeven snapshots gerangschikt in matrices:
DMD zoekt naar een best passende lineaire operator met :
- Bereken de SVD:
- Project:
- Eigenontleding:
- Herstel modi met volledige ruimte:
De kolommen van zijn de DMD-modi; de diagonaal van 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

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 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 eigenwaarden vallen binnen een tolerantie van de eenheidscirkel, waarbij elke oscillatieperiode overeenkomt met in uren (periode staven, geconverteerd), en welk deel van de rendementsvariantie bovenaan staat 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 en vervolgens op het raam , 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: , 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 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 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

Standaard DMD werkt op de onbewerkte toestandsvector. EDMD tilt de gegevens eerst door een woordenboek van niet-lineaire basisfuncties.
Gegeven een woordenboek van scalaire functies , definieer de opgeheven status:
EDMD zoekt met . In de conventie die door de onderstaande code wordt gebruikt – staat als kolommen, links handelend:
| Woordenboektype | Functies | Opnames |
|---|---|---|
| Polynoom | Niet-lineaire interacties tussen activa | |
| Radiale basis (RBF) | Lokale gelijkenis, clustering van regimes | |
| Inbedding met tijdvertraging | Geheugen / autoregressieve structuur | |
| Fourier | Bekende periodiciteiten (intraday, wekelijks) | |
| Volatiliteitskenmerken | 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 , inbeddingsdimensie , 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

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 .
x_k --> [Encoder φ] --> z_k --> [Linear K] --> z_{k+1} --> [Decoder ψ] --> x̂_{k+1}
| |
+--- Linearity loss: ‖z_{k+1} - K z_k‖ ---+
Zonder de 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 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

Rankselectie. De truncatierang 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 expliciet naast elk spectrum dat u publiceert.
Vensterlengte. Koopman-theorie gaat uit van een vaste waarde ; 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 . Oplossingen die de moeite waard zijn om te proberen voordat we concluderen dat de modi onstabiel zijn:
- Totaal DMD (TDMD) — behandelt beide En 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

| 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

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:
- 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.
- 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.
- 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.
- Kies de rang op basis van Marchenko-Pastur, niet door op een elleboog te kijken, en publiceer de rang bij elk spectrum.
- 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.
Auteurs
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.