Koopman Operators and DMD: Do Market Modes Survive Out-of-Sample?
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:
- Modo persistência. Ajustar DMD na janela e na janela . 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.
- Raio espectral como indicador principal. O maior módulo de autovalor é 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

Considere um sistema dinâmico de tempo discreto em um espaço de estados :
onde é um mapa possivelmente não linear. Para os mercados, é um vetor de retornos de ativos, volatilidades ou desequilíbrios na carteira de pedidos no intervalo de tempo .
O operador Koopman não age sobre o estado diretamente, mas em funções observáveis com valor escalar :
A propriedade principal: ** é linear **, mesmo quando não é. O preço é a dimensionalidade - 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

Se tem autovalores e autofunções , então , e qualquer observável na extensão dessas autofunções se decompõe como:
onde 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 codifica uma taxa de crescimento ou decadência () e uma frequência de oscilação ():
| Componente | Propriedade de autovalor | Interpretação financeira |
|---|---|---|
| Tendência | , | Deriva lenta, impulso |
| Ciclos | , | Oscilações, sazonalidade |
| Transientes | Choques decadentes, movimentos de curta duração | |
| Modos instáveis | 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)

DMD é o algoritmo robusto para aproximar a partir de dados. Dados instantâneos organizados em matrizes:
DMD busca um operador linear mais adequado com :
- Calcule o SVD:
- Projeto:
- Eigendecompor:
- Recuperar modos de espaço total:
As colunas de são os modos DMD; a diagonal de 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

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 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 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 barras, convertidas) e qual fração da variação de retorno o topo 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 , então na janela e 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: , 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 é 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 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

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 de funções escalares , defina o estado elevado:
EDMD procura com . Na convenção usada pelo código abaixo - estados como colunas, agindo à esquerda:
| Tipo de dicionário | Funções | Capturas |
|---|---|---|
| Polinômio | Interações não lineares entre ativos | |
| Base radial (RBF) | Similaridade local, agrupamento de regimes | |
| Incorporação com atraso de tempo | Memória/estrutura autoregressiva | |
| Fourier | Periodicidades conhecidas (intradiárias, semanais) | |
| Recursos de volatilidade | 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 , dimensão de incorporação , 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

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

Seleção de classificação. A classificação de truncamento é 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 explicitamente ao lado de cada espectro que você publica.
Comprimento da janela. A teoria de Koopman assume um valor fixo ; 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 . Remédios que vale a pena tentar antes de concluir que os modos são instáveis:
- DMD total (TDMD) — trata ambos e 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

| 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

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:
- 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.
- 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.
- 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.
- Escolha a classificação de Marchenko-Pastur, não olhando para o cotovelo, e publique a classificação com todos os espectros.
- 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.
Authors
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.