← Quay lại danh sách bài viết
August 12, 2026
5 phút đọc

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

Dynamic Mode Decomposition cung cấp cho bạn một phổ: một số eigenvalue phức, mỗi eigenvalue có tốc độ tăng trưởng và tần số, đồng thời gắn với một mode không gian trên cross-section tài sản của bạn. Nó trông giống một cấu trúc. Câu hỏi duy nhất là liệu nó thực sự là cấu trúc hay chỉ là một toán tử tuyến tính đang cần mẫn ghi nhớ một cửa sổ nhiễu.

Câu hỏi đó có hai nửa có thể kiểm định, và bài viết này xoay quanh chúng:

  1. Tính bền của mode. Fit DMD trên cửa sổ tt và cửa sổ t+1t+1. Các mode chính có span cùng một không gian con hay thay đổi sau mỗi lần fit? Nếu chúng thay đổi, DMD chỉ là một phép phân rã trong mẫu và không hơn — nói rõ điều đó hữu ích hơn một tutorial khác.
  2. Spectral radius như chỉ báo dẫn dắt. Modulus của eigenvalue lớn nhất ρ=maxjλj\rho = \max_j |\lambda_j| là một scalar tóm tắt mức độ bùng nổ của động lực học đã fit. Nó dẫn trước realised volatility, trễ nó, hay chỉ nhắc lại nó? Câu trả lời nào cũng có thể công bố; chỉ câu trả lời đầu tiên có thể giao dịch.

Việc thị trường là các hệ thống phi tuyến không dừng được giả định ở đây, không phải lập luận — blog đã lập luận điều đó bằng hình học không gian pha trong attractors trong algotrading và bằng thống kê BTC theo regime được đo lường trong phát hiện regime với HMM. Phần sau tập trung vào câu hỏi hẹp hơn: tính không dừng tác động thế nào đến một toán tử Koopman đã fit, và đo điều đó ra sao.

1. Ý tưởng cốt lõi: Tuyến tính hóa động lực học phi tuyến

Chuyển động thị trường phi tuyến trở thành dòng ẩn tuyến tính

Xét một hệ động lực thời gian rời rạc trên không gian trạng thái MRn\mathcal{M} \subseteq \mathbb{R}^n:

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

trong đó F:MMF: \mathcal{M} \to \mathcal{M} là một ánh xạ có thể phi tuyến. Với thị trường, xkx_k là vector lợi suất tài sản, độ biến động hoặc mất cân bằng sổ lệnh tại bước thời gian kk.

Toán tử Koopman K\mathcal{K} không tác động trực tiếp lên trạng thái xx, mà lên các hàm quan sát có giá trị vô hướng g:MCg: \mathcal{M} \to \mathbb{C}:

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

Tính chất then chốt: K\mathcal{K} là tuyến tính, ngay cả khi FF không tuyến tính. Cái giá phải trả là số chiều — K\mathcal{K} tác động trong một không gian hàm vô hạn chiều. Một xấp xỉ hữu hạn chiều tốt mang lại khả năng biểu đạt của động lực học phi tuyến cùng tính khả thi của đại số tuyến tính: dự báo trở thành lũy thừa ma trận, và mọi mode đều có thể kiểm tra thay vì bị chôn trong trọng số mạng.

2. Phân rã phổ của toán tử Koopman

Các mode phổ mạch lạc của động lực học thị trường

Nếu K\mathcal{K} có các eigenvalue λj\lambda_j và eigenfunction φj\varphi_j, thì Kφj=λjφj\mathcal{K} \varphi_j = \lambda_j \varphi_j, và mọi observable gg trong span của các eigenfunction này được phân rã thành:

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

trong đó vjv_jcác mode Koopman — các hệ số vector mô tả cách mỗi eigenfunction đóng góp vào toàn bộ vector observable.

Mỗi eigenvalue λj=λjeiωj\lambda_j = |\lambda_j| e^{i\omega_j} mã hóa tốc độ tăng trưởng hoặc suy giảm (λj|\lambda_j|) và tần số dao động (ωj\omega_j):

Thành phần Tính chất eigenvalue Diễn giải tài chính
Xu hướng λ1\lambda \approx 1, ω0\omega \approx 0 Trôi chậm, momentum
Chu kỳ λ1\lvert\lambda\rvert \approx 1, ω0\omega \neq 0 Dao động, mùa vụ
Nhất thời λ<1\lvert\lambda\rvert < 1 Cú sốc suy giảm, chuyển động ngắn hạn
Mode không ổn định λ>1\lvert\lambda\rvert > 1 Động lực học tăng trưởng, bùng nổ

Bảng này là lời hứa. Section 4 là nơi lời hứa được kiểm tra bằng dữ liệu.

3. Dynamic Mode Decomposition (DMD)

Các mode động được phân rã rồi lắp ghép lại

DMD là thuật toán workhorse để xấp xỉ K\mathcal{K} từ dữ liệu. Cho các snapshot được sắp xếp thành ma trận:

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 tìm toán tử tuyến tính khớp tốt nhất AA với XAXX' \approx AX:

  1. Tính SVD: X=UΣVX = U \Sigma V^*
  2. Chiếu: A~=UXVΣ1\tilde{A} = U^* X' V \Sigma^{-1}
  3. Phân rã eigen: A~W=WΛ\tilde{A} W = W \Lambda
  4. Khôi phục các mode trong không gian đầy đủ: Φ=XVΣ1W\Phi = X' V \Sigma^{-1} W

Các cột của Φ\Phicác mode DMD; đường chéo của Λ\Lambda chứa các eigenvalue 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

Có hai lựa chọn có chủ ý ở đây. Các mode được chuẩn hóa L2, vì section 4.2 so sánh các không gian con mode giữa các cửa sổ và biên độ chưa chuẩn hóa sẽ lấn át phép so sánh. Biên độ được fit vào snapshot cuối cùng thay vì snapshot đầu tiên, nghĩa là dự báo không bao giờ nâng một eigenvalue lên lũy thừa lớn — nguồn của hiện tượng bùng nổ số học khiến các tín hiệu DMD ngây thơ trông như động lực học trong khi thực chất là tràn số dấu phẩy động.

4. Phép đo

Các quan sát được chuyển thành cấu trúc không gian trạng thái

Đây là phần không nằm trong sách giáo khoa. Mọi thứ ở trên là một quy trình fit; phần dưới là protocol để tìm xem phép fit có ý nghĩa hay không.

Dữ liệu. Dùng dữ liệu sàn riêng của project — cross-section gồm BTC, ETH và các alt thanh khoản trên lưới phút hoặc tick nhất quán, không phải bản tải xuống ETF cổ phiếu theo ngày. Blog ưu tiên crypto và lập luận microstructure không chuyển giao trực tiếp. Xây dựng XX với tài sản trên các hàng và thời gian trên các cột, bằng log return đã demean theo từng tài sản trong mỗi cửa sổ.

4.1 Phổ eigenvalue

Với một cửa sổ đại diện, hãy báo cáo: bao nhiêu trong rr eigenvalue nằm trong vùng dung sai của đường tròn đơn vị, mỗi eigenvalue tương ứng với chu kỳ dao động nào tính bằng giờ (period =2π/ωj= 2\pi / |\omega_j| bars, đã chuyển đổi), và các mode top rr tái tạo bao nhiêu phần phương sai return.

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)

Một báo cáo phổ trung thực tự nó đã đáng giá cho bài viết. Nếu không có gì nằm gần đường tròn đơn vị, thì không có chu kỳ bền nào để giao dịch và câu chuyện "mùa vụ hằng năm" từ văn liệu cổ phiếu đơn giản là không áp dụng cho crypto 24/7.

4.2 Độ ổn định của mode giữa các cửa sổ liền kề

Đây là phép kiểm tra quyết định. Fit DMD trên cửa sổ tt, sau đó trên cửa sổ t+1t+1, rồi đo xem bao nhiêu phần của không gian con mode chính còn lại. Thống kê đúng không phải là tương quan ngây thơ giữa các vector mode — thứ tự mode và pha phức là tùy ý — mà là các góc chính giữa hai không gian con.

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

Hãy báo cáo phân phối của overlap và báo cáo nó đối chiếu với một null: cùng thống kê đó được tính trên các surrogate đã randomize pha của chính các return. Overlap cao nhưng không cao hơn surrogate null có nghĩa là các mode đang theo dõi cấu trúc hiệp phương sai, không phải động lực học.

4.3 Spectral radius cuộn so với realised volatility

Tuyên bố cần kiểm tra là: ρt=maxjλj\rho_t = \max_j |\lambda_j|, được tính lại trên các cửa sổ cuộn, thay đổi trước realised volatility chứ không phải cùng với nó. Scalar này mới về mặt cơ học — blog từng theo dõi các scalar hình học theo thời gian thực, đáng chú ý là độ cong Kobayashi trong đa tạp phức cho giao dịch thuật toán — nhưng modulus eigenvalue Koopman là một đại lượng khác với failure mode khác, nên cần phép kiểm tra lead-lag riêng thay vì kế thừa một lời chào hàng.

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

Hãy align spectral_radius với realised volatility được tính trên cùng grid và đọc peak_lag. Một peak ở lag 0 nghĩa là ρt\rho_t chỉ nhắc lại volatility với thêm vài bước. Một peak ở lag âm, ổn định trên toàn sample và giữa các rank, là dạng duy nhất của bài viết này có một tuyên bố có thể giao dịch.

Điều gì đến sau một kết quả tích cực

Nếu ρt\rho_t dẫn trước, bước tiếp theo hiển nhiên là một construction cross-sectional: xếp hạng tài sản theo lợi suất bước tiếp theo do DMD dự đoán, long các winner được dự đoán và short các loser được dự đoán. Construction này không mới ở đây — đó là factor-residual trade đã được trình bày trong statistical arbitrage và pairs trading trong crypto và section 4 của arbitrage phức với vector và ma trận; điểm riêng thực sự của Koopman duy nhất là eigenportfolio mang eigenvalue biến thiên theo thời gian thay vì loading PCA tĩnh.

Phần strategy được cố ý vắng mặt trong bài viết này vì chưa được backtest ở đây với phí và slippage. Khi được thực hiện, nó phải vượt qua tiêu chuẩn riêng của blog: phép kiểm định ý nghĩa trong Deflated Sharpe Ratio và multiple testing, đối chiếu với counter-example thường trực là kết quả phủ định trung thực. Một plot phổ DMD không phải là kết quả.

Với chính dự báo một bước, implementation đúng dự đoán từ snapshot cuối cùng thay vì truyền eigenvalue lên lũy thừa bằng độ dài cửa sổ:

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. Extended DMD (EDMD): Observable phi tuyến

Trường đặc trưng phi tuyến mở rộng các mode động

DMD tiêu chuẩn hoạt động trên vector trạng thái thô. EDMD trước hết nâng dữ liệu qua một dictionary gồm các hàm cơ sở phi tuyến.

Cho dictionary D={d1,,dp}\mathbf{D} = \{d_1, \ldots, d_p\} gồm các hàm vô hướng di:RnRd_i: \mathbb{R}^n \to \mathbb{R}, hãy định nghĩa trạng thái đã nâng:

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 tìm KRp×pK \in \mathbb{R}^{p \times p} sao cho zk+1Kzkz_{k+1} \approx K z_k. Theo quy ước dùng trong code bên dưới — các state là các cột, KK tác động từ bên trái:

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}

Loại dictionary Hàm Nắm bắt
Đa thức xi, xixj, xi2,x_i,\ x_i x_j,\ x_i^2, \ldots Tương tác phi tuyến giữa các tài sản
Cơ sở xuyên tâm (RBF) exp(γxck2)\exp(-\gamma \lVert x - c_k \rVert^2) Tương đồng cục bộ, gom cụm regime
Nhúng trễ thời gian xk, xk1,, xkτx_k,\ x_{k-1}, \ldots,\ x_{k-\tau} Cấu trúc bộ nhớ / tự hồi quy
Fourier sin(2πfjt), cos(2πfjt)\sin(2\pi f_j t),\ \cos(2\pi f_j t) Chu kỳ đã biết (trong ngày, hàng tuần)
Đặc trưng biến động rt, rt2\lvert r_t \rvert,\ r_t^2 Phương sai thay đổi, gom cụm biến động

Dictionary là nơi kiến thức miền đi vào, và hàng time-delay là lý do đặc thù Koopman để quan tâm đến tọa độ trễ: chúng không phải một kỹ thuật riêng được gắn thêm, mà là một block nữa của ánh xạ nâng. Chính embedding — lựa chọn delay τ\tau, số chiều embedding dd và định lý tái tạo phía sau nó — đã được giới thiệu và viết code trong đa tạp phức cho giao dịch thuật toán; lấy các vector delay từ đó và đưa thẳng vào build_financial_dictionary như các hàng bổ sung.

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

Mạng sâu học động lực học tuyến tính tiềm ẩn

Dictionary EDMD được tạo thủ công, đây là một hạn chế thực sự khi không biết không gian con bất biến Koopman. Deep Koopman networks học lifting và operator cùng lúc.

Kiến trúc là một autoencoder — encoder, latent code, decoder, reconstruction loss, tất cả đã được giới thiệu trong phát hiện bất thường trong algotrading — với một bổ sung là toàn bộ trọng tâm của section: linearity loss, buộc động lực học latent đi qua một ma trận KK được học duy nhất.

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

Không có hạng α\alpha, bạn có một autoencoder thông thường mà latent space tình cờ được theo sau bởi phép nhân ma trận. Với hạng này, mạng bị phạt cho bất kỳ biểu diễn latent nào có diễn tiến không tuyến tính — đó là điều làm cho KK đã học trở thành xấp xỉ Koopman và khiến eigenvalue của nó có thể so sánh với phổ DMD ở section 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 là lý do đáng để huấn luyện mô hình này thay vì dùng sequence model: operator đã học vẫn là một ma trận, vì vậy phép kiểm tra stability ở section 4.2 và phép kiểm tra lead-lag ở section 4.3 áp dụng nguyên vẹn cho mô hình sâu.

7. Cân nhắc thực tế và cạm bẫy

Điều hướng qua drift mô hình và các quan sát thưa

Chọn rank. Rank cắt ngắn rr là một núm bias-variance — quá thấp sẽ bỏ lỡ động lực học, quá cao sẽ fit nhiễu. Đừng nhìn bằng mắt vào khuỷu singular-value; blog đã trả lời đúng câu hỏi "bao nhiêu component trước khi bạn fit nhiễu" bằng biên Marchenko-Pastur từ Random Matrix Theory trong arbitrage phức với vector và ma trận. Giữ các component có singular value vượt biên Marchenko-Pastur cho hình dạng cửa sổ của bạn và nêu rõ rr thu được cạnh mọi phổ bạn công bố.

Độ dài cửa sổ. Lý thuyết Koopman giả định một FF cố định; thị trường không cung cấp một ánh xạ như vậy. Rolling refit là bắt buộc, và stability curve ở section 4.2 chính là chẩn đoán xem cửa sổ được chọn có đủ dài để ước lượng và đủ ngắn để nằm trong một regime hay không.

Độ nhạy với nhiễu. Dữ liệu tài chính có tỷ lệ signal-to-noise thấp và DMD tiêu chuẩn bị bias bởi nhiễu trong XX. Các biện pháp đáng thử trước khi kết luận rằng các mode không ổn định:

  • Total DMD (TDMD) — coi cả XXXX' đều nhiễu thông qua total least squares.
  • Optimized DMD — trực tiếp tối ưu phân rã eigenvalue-mode theo chuẩn Frobenius của phần dư.
  • Kernel EDMD — hoạt động ngầm trong feature space nhiều chiều mà không cần xây dựng dictionary.

Nếu mode stability tăng đáng kể với TDMD, instability là noise đo lường. Nếu không, đó là thị trường.

8. DMD đứng ở đâu

Phân rã mode động giữa các cách tiếp cận mô hình hóa

Phương pháp Tính tuyến tính Có thể diễn giải Dự báo nhiều bước
DMD Tuyến tính trong không gian trạng thái Có (mode + eigenvalue) Ổn định (lũy thừa ma trận)
EDMD Tuyến tính trong không gian nâng Có, nếu biết dictionary Ổn định (lũy thừa ma trận)
Deep Koopman Tuyến tính trong không gian đã học Vừa phải (kiểm tra latent K) Ổn định (lũy thừa ma trận)

Đối với dự báo chuyên biệt cho volatility, điểm so sánh là họ GARCH — xem dự báo volatility crypto bằng GARCH. Với các mô hình chuỗi phi tuyến hoàn toàn và trade-off về khả năng diễn giải cũng như tích lũy sai số của chúng, xem Temporal Fusion Transformer trong trading.

Niche của DMD hẹp nhưng có thật: dự báo nhiều bước được tạo bằng một lũy thừa ma trận duy nhất thay vì autoregressive rollout, với mọi mode đều có thể kiểm tra. Niche đó có alpha hay không là câu hỏi của section 4, không phải của bảng này.

Kết luận

Các quỹ đạo hỗn loạn phân giải thành các mode ổn định

Lý thuyết Koopman là một cách thực sự thanh lịch để nhìn động lực học thị trường, và chính sự thanh lịch là lý do nó cần phép kiểm tra khắc nghiệt nhất có thể. Các điểm chính:

  1. Fit DMD, rồi lập tức kiểm tra phép fit. Overlap của không gian con giữa các cửa sổ liền kề, đo đối chiếu với null đã randomize pha, cho bạn biết trong một buổi chiều liệu đã tìm thấy cấu trúc hay chỉ ghi nhớ một cửa sổ.
  2. Rolling spectral radius là scalar duy nhất đáng theo dõi, và giá trị của nó hoàn toàn phụ thuộc vào kết quả lead-lag. Ở lag 0, nó là proxy của volatility; ở lag âm, nó là cảnh báo regime.
  3. Neo biên độ tại snapshot cuối cùng và không bao giờ nâng eigenvalue lên lũy thừa bằng độ dài cửa sổ. Một phần lớn các "tín hiệu" DMD ngoài thực tế là artifact của số dấu phẩy động.
  4. Chọn rank theo Marchenko-Pastur, không nhìn bằng mắt vào khuỷu, và công bố rank cùng mọi phổ.
  5. Biểu đồ phổ không phải là kết quả. Bất kỳ strategy nào xây trên đó phải vượt qua phí, slippage và một kiểm tra Sharpe đã khử lạm phát trước khi được tính là kết quả.

Với các triển khai vượt ra ngoài những nội dung này, thư viện PyDMD bao quát toàn diện các biến thể DMD, còn code tham chiếu của Mallen et al. bao quát phía deep Koopman.

Thị trường sẽ vẫn hỗn loạn, không dừng và chỉ được quan sát một phần. Lý thuyết Koopman cung cấp một lăng kính có nguyên tắc để trích xuất cấu trúc từ sự hỗn loạn đó — với điều kiện bạn kiểm tra rằng cấu trúc ấy vẫn còn vào tuần tới.


Tài liệu tham khảo và đọc thêm:

  • 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 and J. N. Kutz, "Dynamic Mode Decomposition for Financial Trading Strategies," Quantitative Finance, 2016.
  • A. Mallen et al., "Koopman Neural Forecaster for Time Series with Temporal Distribution Shifts," ICML, 2023.
  • E. Gonzalez and M. Generelo, "Analysis of chaotic economic models through Koopman operators, EDMD, Takens' theorem and Machine Learning," Data Science in Finance and Economics, 2022.
Tuyên bố miễn trừ trách nhiệm: Thông tin được cung cấp trong bài viết này chỉ nhằm mục đích giáo dục và thông tin, không cấu thành lời khuyên về tài chính, đầu tư hoặc giao dịch. Giao dịch tiền mã hóa tiềm ẩn rủi ro thua lỗ đáng kể.

Tác Giả

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

Đi Trước Thị Trường

Đăng ký nhận bản tin của chúng tôi để có những thông tin chuyên sâu độc quyền về AI trading, phân tích thị trường và các cập nhật nền tảng.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất kỳ lúc nào.