← Voltar aos artigos
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

A decomposição em modo dinâmico oferece um espectro: um punhado de autovalores complexos, cada um com uma taxa de crescimento e uma frequência, cada um anexado a um modo espacial na seção transversal de seu ativo. Parece estrutura. A questão toda é se ela é uma estrutura ou se é um operador linear memorizando obedientemente uma janela de ruído.

Essa questão tem duas metades testáveis, e este artigo foi construído em torno delas:

  1. Modo persistência. Ajustar DMD na janela tt e na janela t+1t+1. Os modos dominantes abrangem o mesmo subespaço ou eles reorganizam a cada reforma? Se eles embaralharem, o DMD será uma decomposição dentro da amostra e nada mais - e dizer isso claramente é mais útil do que outro tutorial.
  2. Raio espectral como indicador principal. O maior módulo de autovalor ρ=maxjλj\rho = \max_j |\lambda_j| é um único escalar que resume o quão explosiva é a dinâmica ajustada. Será que lidera a volatilidade percebida, atrasa-a ou apenas a reafirma? Qualquer uma das respostas pode ser publicada; apenas o primeiro é negociável.

Que os mercados são sistemas não-lineares não estacionários é assumido aqui, não discutido - o blog já argumentou isso, com geometria de espaço de fase em atratores em algotrading e com estatísticas BTC medidas por regime em detecção de regime com HMMs. O que se segue é a questão mais restrita do que a não estacionariedade faz com um operador de Koopman ajustado e como medi-la.

1. A ideia central: Linearização da dinâmica não linear

Movimento não linear do mercado tornando-se fluxo latente linear

Considere um sistema dinâmico de tempo discreto em um espaço de estados MRn\mathcal{M} \subseteq \mathbb{R}^n:

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

onde F:MMF: \mathcal{M} \to \mathcal{M} é um mapa possivelmente não linear. Para os mercados, xkx_k é um vetor de retornos de ativos, volatilidades ou desequilíbrios na carteira de pedidos no intervalo de tempo kk.

O operador Koopman K\mathcal{K} não age sobre o estado xx diretamente, mas em funções observáveis ​​com valor escalar g:MCg: \mathcal{M} \to \mathbb{C}:

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

A propriedade principal: **K\mathcal{K} é linear **, mesmo quando FF não é. O preço é a dimensionalidade - K\mathcal{K} atua em um espaço funcional de dimensão infinita. Uma boa aproximação de dimensão finita proporciona a expressividade da dinâmica não linear com a tratabilidade da álgebra linear: a previsão torna-se exponenciação de matriz e cada modo é inspecionável em vez de enterrado em pesos de rede.

2. Decomposição Espectral do Operador Koopman

Modos espectrais coerentes de dinâmica de mercado

Se K\mathcal{K} tem autovalores λj\lambda_j e autofunções φj\varphi_j, então Kφj=λjφj\mathcal{K} \varphi_j = \lambda_j \varphi_j, e qualquer observável gg na extensão dessas autofunções se decompõe como:

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

onde vjv_j são os modos Koopman — coeficientes com valor vetorial que descrevem como cada função própria contribui para o vetor observável completo.

Cada autovalor λj=λjeiωj\lambda_j = |\lambda_j| e^{i\omega_j} codifica uma taxa de crescimento ou decadência (λj|\lambda_j|) e uma frequência de oscilação (ωj\omega_j):

Componente Propriedade de autovalor Interpretação financeira
Tendência λ1\lambda \approx 1, ω0\omega \approx 0 Deriva lenta, impulso
Ciclos λ1\lvert\lambda\rvert \approx 1, ω0\omega \neq 0 Oscilações, sazonalidade
Transientes λ<1\lvert\lambda\rvert < 1 Choques decadentes, movimentos de curta duração
Modos instáveis ​​ λ>1\lvert\lambda\rvert > 1 Dinâmica crescente e explosiva

Esta mesa é a promessa. A seção 4 é onde a promessa é verificada em relação aos dados.

3. Decomposição em modo dinâmico (DMD)

Modos dinâmicos decompostos e remontados

DMD é o algoritmo robusto para aproximar K\mathcal{K} a partir de dados. Dados instantâneos organizados em matrizes:

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 busca um operador linear mais adequado AA com XAXX' \approx AX:

  1. Calcule o SVD: X=UΣVX = U \Sigma V^*
  2. Projeto: A~=UXVΣ1\tilde{A} = U^* X' V \Sigma^{-1}
  3. Eigendecompor: A~W=WΛ\tilde{A} W = W \Lambda
  4. Recuperar modos de espaço total: Φ=XVΣ1W\Phi = X' V \Sigma^{-1} W

As colunas de Φ\Phi são os modos DMD; a diagonal de Λ\Lambda contém os valores próprios DMD.

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

Duas escolhas deliberadas aqui. Os modos são normalizados em L2, porque a seção 4.2 compara subespaços de modo entre janelas e amplitudes não normalizadas inundariam a comparação. As amplitudes são ajustadas ao último instantâneo e não ao primeiro, o que significa que uma previsão nunca eleva um autovalor a uma grande potência - a fonte da explosão numérica que faz com que os sinais DMD ingênuos pareçam dinâmicos quando são estouro de ponto flutuante.

4. A Medição

Observações transformadas em estrutura de espaço de estados

Esta é a parte do artigo que não é um livro didático. Tudo acima é um procedimento adequado; abaixo está o protocolo para descobrir se o ajuste significa alguma coisa.

Dados. Use os dados de exchange do próprio projeto — um corte transversal de BTC, ETH e alts líquidos em uma grade consistente de minutos ou ticks, e não um download diário de ETFs de ações. O blog prioriza a criptografia e o argumento da microestrutura não é transferido. Construir XX com ativos em linhas e tempo em colunas, em retornos de log, rebaixados por ativo em cada janela.

4.1 O espectro de autovalores

Relate, para uma janela representativa: quantos dos rr autovalores ficam dentro de uma tolerância do círculo unitário, a qual período de oscilação cada um corresponde em horas (período =2π/ωj= 2\pi / |\omega_j| barras, convertidas) e qual fração da variação de retorno o topo rr modos reconstruir.

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)

Um relatório honesto sobre o espectro já vale o artigo. Se nada ficar perto do círculo unitário, não haverá ciclos persistentes de negociação e a história de “sazonalidade anual” da literatura sobre ações simplesmente não se aplica à criptografia 24 horas por dia, 7 dias por semana.

4.2 Estabilidade do modo em janelas adjacentes

O teste decisivo. Ajustar DMD na janela tt, então na janela t+1t+1e medir quanto do subespaço do modo dominante sobrevive. A estatística correta não é uma correlação ingênua de vetores de modo - a ordem de modo e a fase complexa são arbitrárias - mas os ângulos principais entre os dois subespaços.

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

Relate a distribuição dessa sobreposição e relate-a contra um valor nulo: a mesma estatística calculada em substitutos randomizados por fase dos mesmos retornos. A sobreposição alta, mas não superior ao nulo substituto, significa que os modos estão rastreando a estrutura de covariância, não a dinâmica.

4.3 Raio espectral rolante contra volatilidade realizada

A reivindicação para testar: ρt=maxjλj\rho_t = \max_j |\lambda_j|, recalculado em janelas rolantes, move-se antes da volatilidade percebida, e não junto com ela. O escalar é mecanicamente novo - o blog já monitorou escalares geométricos em tempo real antes, notavelmente a curvatura de Kobayashi em variedades complexas para negociação algorítmica - mas um módulo de autovalor de Koopman é uma quantidade diferente com um modo de falha diferente e merece seu próprio teste de lead-lag em vez de um passo herdado.

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

Alinhar spectral_radius à volatilidade realizada calculada na mesma grade e lida peak_lag. Um pico no atraso 0 significa ρt\rho_t é uma reformulação da volatilidade com etapas extras. Um pico no desfasamento negativo, estável em toda a amostra e em todas as classificações, é a única versão deste artigo com uma reivindicação negociável.

Uma nota sobre o que vem depois de um resultado positivo

Se ρt\rho_t lidera, o próximo passo óbvio é a construção transversal: classificar os ativos pelo retorno do próximo passo previsto pelo DMD, operar comprado nos vencedores previstos e vendido nos perdedores previstos. Essa construção não é nova aqui — é a negociação fator-residual já abordada em arbitragem estatística e negociação de pares em criptografia e na seção 4 de arbitragem complexa com vetores e matrizes; a única reviravolta genuinamente específica de Koopman é que os portfólios próprios carregam valores próprios que variam no tempo, em vez de cargas estáticas de PCA.

A seção de estratégia está deliberadamente ausente deste artigo porque não foi testada aqui com taxas e slippage. Quando for, ele tem que limpar a própria barra do blog: o teste de significância em o índice de Sharpe deflacionado e testes múltiplos, contra o contra-exemplo permanente do negativo honesto. Um gráfico do espectro DMD não é um resultado.

Para a previsão de uma etapa em si, a implementação correta prevê a partir do último instantâneo, em vez de propagar um autovalor à potência do comprimento da janela:

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. DMD Estendido (EDMD): Observáveis Não Lineares

Campo de recurso não linear estendendo modos dinâmicos

O DMD padrão opera no vetor de estado bruto. O EDMD primeiro eleva os dados por meio de um dicionário de funções de base não linear.

Dado um dicionário D={d1,,dp}\mathbf{D} = \{d_1, \ldots, d_p\} de funções escalares di:RnRd_i: \mathbb{R}^n \to \mathbb{R}, defina o estado elevado:

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 procura KRp×pK \in \mathbb{R}^{p \times p} com zk+1Kzkz_{k+1} \approx K z_k. Na convenção usada pelo código abaixo - estados como colunas, KK agindo à esquerda:

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}

Tipo de dicionário Funções Capturas
Polinômio xi, xixj, xi2,x_i,\ x_i x_j,\ x_i^2, \ldots Interações não lineares entre ativos
Base radial (RBF) exp(γxck2)\exp(-\gamma \lVert x - c_k \rVert^2) Similaridade local, agrupamento de regimes
Incorporação com atraso de tempo xk, xk1,, xkτx_k,\ x_{k-1}, \ldots,\ x_{k-\tau} Memória/estrutura autoregressiva
Fourier sin(2πfjt), cos(2πfjt)\sin(2\pi f_j t),\ \cos(2\pi f_j t) Periodicidades conhecidas (intradiárias, semanais)
Recursos de volatilidade rt, rt2\lvert r_t \rvert,\ r_t^2 Heterocedasticidade, agrupamento vol

O dicionário é onde entra o conhecimento do domínio, e a linha de atraso de tempo é a razão específica de Koopman para se preocupar com as coordenadas de atraso: elas não são uma técnica separada, são mais um bloco do mapa de elevação. A incorporação em si – escolha do atraso τ\tau, dimensão de incorporação dd, e o teorema de reconstrução por trás dele — já foi introduzido e codificado em variedades complexas para negociação algorítmica; pegue os vetores de atraso de lá e alimente-os diretamente build_financial_dictionary como linhas extras.

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. Redes Deep Koopman

Dinâmica linear latente de aprendizagem de rede profunda

O dicionário EDMD é feito à mão, o que é uma limitação real quando o subespaço invariante de Koopman é desconhecido. As redes Deep Koopman aprendem o levantamento e o operador em conjunto.

A arquitetura é um autoencoder — codificador, código latente, decodificador, perda de reconstrução, tudo conforme introduzido em detecção de anomalias em algotrading — com uma adição que é o ponto principal da seção: uma perda de linearidade forçando a dinâmica latente através de uma única matriz aprendida 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}}

Sem o α\alpha termo, você tem um autoencoder comum cujo espaço latente é seguido por uma multiplicação de matriz. Com isso, a rede é penalizada por qualquer representação latente cuja evolução não seja linear — que é o que torna o aprendido KK uma aproximação de Koopman e seus autovalores comparáveis ​​ao espectro DMD da seção 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 é o que faz com que valha a pena treinar, em vez de buscar um modelo de sequência: o operador aprendido ainda é uma matriz, portanto, o teste de estabilidade da seção 4.2 e o teste de lead-lag da seção 4.3 se aplicam inalterados a um modelo profundo.

7. Considerações práticas e armadilhas

Navegando pela deriva do modelo e observações esparsas

Seleção de classificação. A classificação de truncamento rr é um dial de variação de polarização - dinâmica de erros muito baixa, ruído de ajuste muito alto. Não observe um cotovelo de valor singular; o blog já responde "quantos componentes antes de você ajustar o ruído" corretamente, com o Marchenko-Pastur ligado à Teoria de Matrizes Aleatórias em arbitragem complexa com vetores e matrizes. Mantenha os componentes cujos valores singulares excedem a borda de Marchenko-Pastur para o formato da sua janela e indique o resultado rr explicitamente ao lado de cada espectro que você publica.

Comprimento da janela. A teoria de Koopman assume um valor fixo FF; os mercados não fornecem um. As reformas contínuas são obrigatórias, e a curva de estabilidade da seção 4.2 é precisamente o diagnóstico para saber se a janela escolhida é longa o suficiente para ser estimada e curta o suficiente para permanecer dentro de um regime.

Sensibilidade ao ruído. Os dados financeiros têm uma relação sinal-ruído baixa e o DMD padrão é influenciado pelo ruído XX. Remédios que vale a pena tentar antes de concluir que os modos são instáveis:

  • DMD total (TDMD) — trata ambos XX e XX' tão barulhento por meio de mínimos quadrados totais.
  • DMD otimizado — otimiza diretamente a decomposição do modo de autovalor em relação à norma residual de Frobenius.
  • Kernel EDMD — funciona implicitamente em um espaço de recursos de alta dimensão sem construir o dicionário.

Se a estabilidade do modo aumentar materialmente sob o TDMD, a instabilidade foi ruído de medição. Se não, foi o mercado.

8. Onde fica o DMD

Decomposição do modo dinâmico entre abordagens de modelagem

Método Linearidade Interpretável Previsão em várias etapas
DMD Linear no espaço de estados Sim (modos + valores próprios) Estável (potência da matriz)
EDMD Linear em espaço elevado Sim, dado o dicionário Estável (potência da matriz)
Profundo Koopman Linear no espaço aprendido Moderado (inspecione K latente) Estável (potência da matriz)

Para previsões específicas de volatilidade, o ponto de comparação é a família GARCH — consulte Previsão de volatilidade GARCH para criptografia. Para modelos de sequência totalmente não lineares e as compensações de interpretabilidade e acumulação de erros que eles carregam, consulte o Transformador de Fusão Temporal na negociação.

O nicho que o DMD ocupa é estreito, mas real: previsões em várias etapas geradas por um único poder de matriz, em vez de implementação autoregressiva, com todos os modos inspecionáveis. Se esse nicho contém alfa é uma questão da seção 4, não desta tabela.

Conclusão

Trajetórias turbulentas resolvendo-se em modos estáveis

A teoria de Koopman é uma maneira genuinamente elegante de observar a dinâmica do mercado, e a elegância é exatamente a razão pela qual ela precisa do teste mais severo disponível. As conclusões:

  1. Ajuste o DMD e teste imediatamente o ajuste. A sobreposição de subespaço entre janelas adjacentes, medida em relação a um nulo randomizado por fase, informa em uma tarde se você encontrou uma estrutura ou memorizou uma janela.
  2. O raio espectral móvel é o escalar que vale a pena monitorar e seu valor depende inteiramente do resultado de lead-lag. No lag 0 é uma proxy de volatilidade; no atraso negativo é um aviso do regime.
  3. Ancorar amplitudes no último instantâneo e nunca aumentar os autovalores para o comprimento da janela. Uma grande fração dos "sinais" DMD existentes são artefatos de ponto flutuante.
  4. Escolha a classificação de Marchenko-Pastur, não olhando para o cotovelo, e publique a classificação com todos os espectros.
  5. Um gráfico de espectro não é um resultado. Qualquer estratégia construída sobre isso tem que sobreviver a taxas, derrapagens e a um teste de Sharpe vazio antes de contar.

Para implementações além dessas, a biblioteca PyDMD cobre as variantes DMD de forma abrangente, e o código de referência de Mallen et al. cobre o lado profundo de Koopman.

Os mercados permanecerão confusos, não estacionários e parcialmente observados. A teoria de Koopman oferece uma lente de princípios para extrair estrutura dessa bagunça – desde que você verifique se a estrutura ainda está lá na próxima semana.


Referências e leituras adicionais:- B. O. Koopman, "Sistemas Hamiltonianos e Transformação no Espaço Hilbert", 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, "Uma aproximação baseada em dados do operador Koopman: Estendendo a decomposição do modo dinâmico", Journal of Nonlinear Science, 2015.
  • B. Lusch, JN Kutz, SL Brunton, "Aprendizado profundo para embeddings lineares universais de dinâmica não linear", Nature Communications, 2018.
  • J. Mann e J. N. Kutz, "Decomposição do modo dinâmico para estratégias de negociação financeira", Finanças quantitativas, 2016.
  • A. Mallen et al., "Koopman Neural Forecaster for Time Series with Temporal Distribution Shifts", ICML, 2023.
  • E. Gonzalez e M. Generelo, "Análise de modelos econômicos caóticos por meio de operadores Koopman, EDMD, teorema de Takens e aprendizado de máquina", 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

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.