Koopman Operators and DMD: Do Market Modes Survive Out-of-Sample?
Декомпозиция динамических мод дает спектр: несколько комплексных собственных значений, каждое со скоростью роста и частотой, а также с пространственной модой по всему набору активов. Это похоже на структуру. Вопрос в том, действительно ли это структура, или линейный оператор просто послушно запомнил одно окно шума.
Этот вопрос состоит из двух проверяемых частей, и эта статья построена вокруг них:
- Сохранение мод. Обучите DMD на окне и на окне . Доминирующие моды охватывают одно и то же подпространство или меняются при каждом переобучении? Если они меняются, DMD дает внутривыборочную декомпозицию и ничего больше — прямо сказать это полезнее, чем публиковать еще один учебник.
- Спектральный радиус как опережающий индикатор. Наибольший модуль собственных значений — это один скаляр, суммирующий взрывоопасность подобранной динамики. Он опережает реализованную волатильность, отстает от нее или просто пересказывает ее? Любой ответ достоин публикации; торговать можно только на первом.
То, что рынки являются нестационарными нелинейными системами, здесь предполагается, а не доказывается: в блоге это уже обсуждалось через геометрию фазового пространства в статье Аттракторы в алгоритмическом трейдинге и через измеренную статистику BTC для каждого режима в статье Обнаружение режима с помощью HMM. Далее рассматривается более узкий вопрос: что нестационарность делает с подогнанным оператором Купмана и как это измерить.
1. Основная идея: линеаризация нелинейной динамики

Рассмотрим динамическую систему с дискретным временем на пространстве состояний :
где — возможно, нелинейное отображение. Для рынка — это вектор доходностей активов, волатильностей или дисбалансов книги ордеров на временном шаге .
Оператор Купмана действует не непосредственно на состояние , а на скалярные наблюдаемые функции :
Ключевое свойство: линейен, даже если нелинейна. Цена — размерность: действует в бесконечномерном функциональном пространстве. Хорошая конечномерная аппроксимация дает выразительность нелинейной динамики и удобство линейной алгебры: прогнозирование превращается в возведение матрицы в степень, а каждую моду можно исследовать, вместо того чтобы искать ее в весах сети.
2. Спектральное разложение оператора Купмана

Если имеет собственные значения и собственные функции , то , а любую наблюдаемую в линейной оболочке этих собственных функций можно разложить так:
где — моды Купмана, то есть векторные коэффициенты, описывающие вклад каждой собственной функции в полный наблюдаемый вектор.
Каждое собственное значение кодирует скорость роста или затухания () и частоту колебаний ():
| Компонент | Свойство собственного значения | Финансовая интерпретация |
|---|---|---|
| Тренд | , | Медленный дрейф, импульс |
| Циклы | , | Колебания, сезонность |
| Переходные процессы | Затухающие шоки, краткоживущие движения | |
| Нестабильные моды | Растущая, взрывная динамика |
Эта таблица — обещание. В разделе 4 обещание проверяется на соответствие данным.
3. Динамическая модовая декомпозиция (DMD)

DMD — это основной алгоритм аппроксимации по данным. Пусть снимки состояния собраны в матрицы: DMD ищет наилучший линейный оператор , для которого :
- Вычислить SVD: .
- Спроецировать: .
- Выполнить спектральное разложение: .
- Восстановить моды полного пространства: .
Столбцы — это моды DMD, а диагональ содержит собственные значения 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
Здесь сделаны два осознанных выбора. Моды нормализованы по L2, поскольку в разделе 4.2 сравниваются подпространства мод в разных окнах, а ненормализованные амплитуды исказили бы сравнение. Амплитуды подбираются по последнему снимку, а не по первому, поэтому прогноз никогда не возводит собственное значение в большую степень — именно это вызывает численный взрыв и заставляет наивные сигналы DMD выглядеть как динамика, хотя на деле это переполнение с плавающей точкой.
4. Измерение

Это часть статьи, которая выходит за рамки учебника. Все вышеописанное — процедура подгонки; ниже приведен протокол, позволяющий выяснить, означает ли эта подгонка хоть что-нибудь.
Данные. Используйте собственные биржевые данные проекта — срез BTC, ETH и ликвидных альткоинов на единой минутной или тиковой сетке, а не ежедневную выгрузку фондовых ETF. Блог ориентирован на крипторынок, поэтому аргумент о микроструктуре не переносится автоматически. Постройте с активами в строках и временем в столбцах, используя логарифмические доходности, центрированные по каждому активу внутри каждого окна.
4.1 Спектр собственных значений
Для репрезентативного окна сообщите, сколько из собственных значений попадает в заданный допуск от единичной окружности, какому периоду колебаний соответствует каждое значение в часах (период баров с последующим пересчетом), и какую долю дисперсии доходностей восстанавливают верхних мод.
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)
Честный отчет о спектре уже делает статью полезной. Если ничего не находится рядом с единичной окружностью, значит, устойчивых циклов для торговли нет, и история о "годовой сезонности" из литературы по акциям не переносится на крипторынок, работающий 24/7.
4.2 Стабильность режима в соседних окнах
Это решающий тест. Обучите DMD на окне , затем на окне и измерьте, какая часть подпространства доминирующих мод сохраняется. Нужная статистика — не наивная корреляция векторов мод (порядок мод и комплексная фаза произвольны), а главные углы между двумя подпространствами.
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)])
Сообщите распределение этого перекрытия и сравните его с нулевым распределением: той же статистикой, рассчитанной на фазово-рандомизированных суррогатах тех же доходностей. Большое перекрытие, не превышающее суррогатный ноль, означает, что моды отслеживают ковариационную структуру, а не динамику.
4.3 Изменение спектрального радиуса в зависимости от реализованной волатильности
Проверяемое утверждение таково: , пересчитанный в скользящих окнах, движется до реализованной волатильности, а не вместе с ней. Сам скаляр не совсем новый — блог уже отслеживал геометрические скаляры в реальном времени, в частности кривизну Кобаяши в статье Сложные многообразия для алгоритмической торговли, — но модуль собственного значения Купмана является другой величиной с другим режимом отказа. Он заслуживает отдельного теста опережения и запаздывания, а не унаследованного рекламного тезиса.
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))])}
Выровняйте spectral_radius с реализованной волатильностью, рассчитанной на той же сетке, и определите peak_lag. Пик при лаге 0 означает, что всего лишь пересказывает волатильность с дополнительными шагами. Отрицательный лаг, устойчивый на всей выборке и для разных рангов, — единственный вариант этой статьи, который содержит торговое утверждение.
Примечание о том, что происходит после положительного результата
Если действительно опережает волатильность, очевидным следующим шагом станет кросс-секционная конструкция: ранжировать активы по прогнозируемой DMD доходности следующего шага, покупать предполагаемых победителей и шортить предполагаемых проигравших. Сама конструкция не нова — это торговля факторным остатком, уже описанная в статье Статистический арбитраж и парная торговля криптовалютой и в разделе 4 книги Комплексный арбитраж с векторами и матрицами. Специфический для Купмана поворот в том, что собственные портфели несут меняющиеся во времени собственные значения, а не статические нагрузки PCA.
Раздел со стратегией намеренно отсутствует: здесь она не тестировалась на истории с учетом комиссий и проскальзывания. Когда тест появится, стратегия должна пройти собственную планку блога — проверку значимости из статьи Дефлированный коэффициент Шарпа и множественное тестирование и сравнение с постоянным контрпримером Честный отрицательный результат. График спектра DMD не является результатом.
Для самого одношагового прогноза правильная реализация использует последний снимок, а не возводит собственное значение в степень, равную длине окна:
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 (EDMD): нелинейные наблюдаемые

Стандартный DMD работает с исходным вектором состояния. EDMD сначала поднимает данные через словарь нелинейных базисных функций.
Пусть дан словарь скалярных функций . Определим поднятое состояние:
EDMD ищет , для которого . В соглашении, использованном в коде ниже, состояния записываются как столбцы, а действует слева:
| Тип словаря | Функции | Захватывает |
|---|---|---|
| Полином | Нелинейные взаимодействия между активами | |
| Радиальный базис (RBF) | Локальное сходство, кластеризация режимов | |
| Встраивание задержек | Память / авторегрессионная структура | |
| Фурье | Известные периодичности (внутридневные, еженедельные) | |
| Признаки волатильности | Гетероскедастичность, кластеризация волатильности |
Словарь — место, где используется предметная область, а строка с временными задержками — специфичная для Купмана причина вообще учитывать координаты задержек: это не отдельная пристегнутая техника, а еще один блок отображения подъема. Само встраивание — выбор задержки , размерности и лежащей в его основе теоремы реконструкции — уже введено и закодировано в статье Сложные многообразия для алгоритмического трейдинга. Возьмите оттуда векторы задержек и передайте их прямо в build_financial_dictionary как дополнительные строки.
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. Глубокие сети Купмана

Словарь EDMD создается вручную, что становится серьезным ограничением, когда неизвестно инвариантное подпространство Купмана. Глубокие сети Купмана одновременно обучают отображение подъема и оператор.
Архитектура представляет собой автокодировщик — кодер, скрытый код, декодер и функцию потерь реконструкции, как описано в статье Обнаружение аномалий в алгоритмическом трейдинге. Дополнение, составляющее весь смысл раздела, — потеря линейности, заставляющая скрытую динамику проходить через одну обучаемую матрицу .
x_k --> [Encoder φ] --> z_k --> [Linear K] --> z_{k+1} --> [Decoder ψ] --> x̂_{k+1}
| |
+--- Linearity loss: ‖z_{k+1} - K z_k‖ ---+
Без члена мы получаем обычный автокодировщик, за скрытым пространством которого просто следует умножение матрицы. При наличии этого члена сеть штрафуется за любое скрытое представление, эволюция которого не линейна. Именно это превращает обученную в аппроксимацию Купмана, а ее собственные значения делает сопоставимыми со спектром DMD из раздела 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
Именно поэтому стоит обучать такую модель, а не сразу обращаться к модели последовательностей: обученный оператор по-прежнему является матрицей, поэтому тест стабильности из раздела 4.2 и тест опережения и запаздывания из раздела 4.3 применимы к глубокой модели без изменений.
7. Практические соображения и подводные камни

Выбор ранга. Ранг усечения задает компромисс между смещением и дисперсией: слишком низкий ранг пропускает динамику, слишком высокий подгоняет шум. Не выбирайте ранг по визуальному "локтю" сингулярных значений. В статье Комплексный арбитраж с векторами и матрицами уже правильно разобрано, "сколько компонент можно оставить до подгонки шума", с использованием границы Марченко — Пастура из теории случайных матриц. Сохраняйте компоненты, чьи сингулярные значения превышают границу Марченко — Пастура для формы вашего окна, и явно указывайте получившийся рядом с каждым публикуемым спектром.
Длина окна. Теория Купмана предполагает фиксированное , но рынки его не предоставляют. Скользящее переобучение обязательно, а кривая стабильности из раздела 4.2 — именно тот диагностический показатель, который отвечает, достаточно ли выбранное окно длинное для оценки и достаточно ли короткое, чтобы оставаться внутри одного режима.
Чувствительность к шуму. Финансовые данные имеют низкое отношение сигнал/шум, а стандартный DMD искажается шумом в . Прежде чем заключать, что моды нестабильны, стоит попробовать:
- Total DMD (TDMD) — считает и , и зашумленными, используя метод полных наименьших квадратов.
- Оптимизированный DMD — напрямую оптимизирует разложение на собственные значения и моды по остаточной норме Фробениуса.
- Kernel EDMD — неявно работает в многомерном пространстве признаков, не строя словарь.
Если при TDMD стабильность мод существенно возрастает, нестабильность была вызвана шумом измерения. Если нет, причина — сам рынок.
8. Где находится DMD

| Метод | Линейность | Интерпретируемость | Многошаговый прогноз |
|---|---|---|---|
| DMD | Линейный в пространстве состояний | Да (моды + собственные значения) | Стабильный (степень матрицы) |
| EDMD | Линейный в поднятом пространстве | Да, если известен словарь | Стабильный (степень матрицы) |
| Глубокий Купман | Линейный в обученном пространстве | Умеренная (проверяйте скрытую K) | Стабильный (степень матрицы) |
Для прогнозирования волатильности точкой сравнения служит семейство GARCH — см. Прогнозирование волатильности GARCH для криптовалют. О полностью нелинейных моделях последовательностей и компромиссах между интерпретируемостью и накоплением ошибок см. статью Temporal Fusion Transformer в торговле.
Ниша DMD узка, но реальна: многошаговые прогнозы, порождаемые одной степенью матрицы, а не авторегрессионным развертыванием, при этом каждую моду можно исследовать. Содержит ли эта ниша альфу — вопрос раздела 4, а не этой таблицы.
Заключение

Теория Купмана — действительно элегантный способ взглянуть на динамику рынка, и именно поэтому она требует самой строгой проверки. Основные выводы:
- Подгоните DMD, а затем немедленно проверьте подгонку. Перекрытие подпространств соседних окон, измеренное относительно фазово-рандомизированного нулевого распределения, за один рабочий день покажет, нашли ли вы структуру или просто запомнили окно.
- Скользящий спектральный радиус — единственный скаляр, который стоит отслеживать, но его смысл полностью зависит от результата теста опережения и запаздывания. При лаге 0 это индикатор волатильности, при отрицательном лаге — предупреждение о режиме.
- Привязывайте амплитуды к последнему снимку и никогда не возводите собственные значения в степень, равную длине окна. Большая часть "сигналов" DMD в реальных данных — артефакты вычислений с плавающей точкой.
- Выбирайте ранг по границе Марченко — Пастура, а не по визуальному локтю, и публикуйте ранг вместе с каждым спектром.
- График спектра не является результатом. Любая стратегия на его основе должна пережить комиссии, проскальзывание и дефлированный тест Шарпа, прежде чем ее можно будет считать рабочей.
Для других реализаций библиотека PyDMD полно охватывает варианты DMD, а эталонный код Маллена и др. — глубокую сторону Купмана.
Рынки останутся шумными, нестационарными и лишь частично наблюдаемыми. Теория Купмана предлагает принципиальный способ извлекать структуру из этого хаоса — при условии, что на следующей неделе вы проверите, сохранилась ли эта структура.
Ссылки и дополнительная литература:
- Б. О. Купман, "Гамильтоновы системы и преобразования в гильбертовом пространстве", * Труды Национальной академии наук *, 1931.
- Дж. Х. Ту и др., "О динамической модовой декомпозиции: теория и приложения", Журнал вычислительной динамики, 2014.
- М. О. Уильямс, И. Г. Кеврекидис, К. В. Роули, "Управляемая данными аппроксимация оператора Купмана: расширение динамической модовой декомпозиции", *Журнал нелинейной науки *, 2015.
- Б. Луш, Дж. Н. Куц, С. Л. Брантон, "Глубокое обучение для универсальных линейных вложений нелинейной динамики", Nature Communications, 2018.
- Дж. Манн и Дж. Н. Куц, "Динамическая модовая декомпозиция для финансовых торговых стратегий", Количественные финансы, 2016.
- А. Маллен и др., "Нейронный прогнозист Купмана для временных рядов со сдвигами временного распределения", ICML, 2023.
- Э. Гонсалес и М. Дженерело, "Анализ хаотических экономических моделей с помощью операторов Купмана, EDMD, теоремы Такенса и машинного обучения", Наука о данных в финансах и экономике, 2022.
Авторы
Инженер торговых систем
Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.