← 記事一覧に戻る
August 12, 2026
読了時間: 5分

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

動的モード分解により、スペクトルが提供されます。スペクトルとは、成長率と周波数を備えたいくつかの複雑な固有値であり、それぞれが資産断面上の空間モードに関連付けられています。構造っぽいですね。問題全体は、それが構造体であるのか、それとも 1 つのノイズ ウィンドウを忠実に記憶する線形演算子であるのかということです。

この質問にはテスト可能な 2 つの部分があり、この記事はそれらを中心に構成されています。

  1. モードの永続性。 DMD をウィンドウ tt とウィンドウ t+1t+1 にフィットさせます。支配的なモードは同じ部分空間にまたがるのか、それとも再調整されるたびに再シャッフルされるのか?再シャッフルする場合、DMD はサンプル内の分解に過ぎず、それ以上のことはありません。そして、それをはっきりと言うことは、別のチュートリアルよりも有益です。
  2. 先行指標としてのスペクトル半径。 最大固有値係数 ρ=maxjλj\rho = \max_j |\lambda_j| は、適合されたダイナミクスがどれほど爆発的であるかを要約する単一のスカラーです。それは現実のボラティリティを「先導」しているのでしょうか、遅らせているのでしょうか、あるいは単にそれを言い直しているだけなのでしょうか?どちらの回答も公開可能です。最初のものだけが取引可能です。

市場が非定常非線形システムであることは、ここでは仮定されており、議論されていません。このブログでは、algotrading のアトラクターの位相空間幾何学と、HMMs による体制検出] の体制ごとの測定された BTC 統計を使用して、すでにそれを主張しています。以下に続くのは、非定常性が 適合されたクープマン演算子 に与える影響と、それをどのように測定するかというより狭い問題です。

1. 核となるアイデア: 非線形ダイナミクスの線形化

非線形の市場運動が線形の潜在フローになる

状態空間 MRn\mathcal{M} \subseteq \mathbb{R}^n 上の離散時間動的システムを考えてみましょう。

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

ここで、F:MMF: \mathcal{M} \to \mathcal{M} は非線形マップである可能性があります。市場の場合、xkx_k は、タイム ステップ kk における資産リターン、ボラティリティ、または注文帳の不均衡のベクトルです。

Koopman 演算子 K\mathcal{K} は、状態 xx に直接作用するのではなく、スカラー値の観測可能な関数 g:MCg: \mathcal{M} \to \mathbb{C} に作用します。

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

重要なプロパティ: K\mathcal{K} は、FF がそうでない場合でも線形です。代償は次元です。K\mathcal{K} は無限次元の関数空間に作用します。優れた有限次元近似により、線形代数の扱いやすさを備えた非線形ダイナミクスの表現力が得られます。予測は行列のべき乗となり、すべてのモードがネットワークの重みに埋もれるのではなく検査可能になります。

2. Koopman作用素のスペクトル分解

市場ダイナミクスのコヒーレントなスペクトル モード

K\mathcal{K} に固有値 λj\lambda_j と固有関数 φj\varphi_j がある場合、Kφj=λjφj\mathcal{K} \varphi_j = \lambda_j \varphi_j、およびこれらの固有関数の範囲内の観測可能な gg は次のように分解されます。

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

ここで、vjv_jクープマン モード、つまり各固有関数が完全な観測可能なベクトルにどのように寄与するかを記述するベクトル値の係数です。

各固有値 λj=λjeiωj\lambda_j = |\lambda_j| e^{i\omega_j} は、成長率または減衰率 (λj|\lambda_j|) と発振周波数 (ωj\omega_j) をエンコードします。

コンポーネント 固有値プロパティ 財務解釈
トレンド λ1\lambda \approx 1ω0\omega \approx 0 ゆっくりとしたドリフト、勢い
サイクル λ1\lvert\lambda\rvert \approx 1ω0\omega \neq 0 振動、季節性
トランジェント λ<1\lvert\lambda\rvert < 1 減衰するショック、短命な動き
不安定なモード λ>1\lvert\lambda\rvert > 1 成長する爆発的なダイナミクス

このテーブルはお約束です。セクション 4 では、Promise がデータに対してチェックされます。

3. 動的モード分解 (DMD)

動的モードの分解と再構築

DMD は、データから K\mathcal{K} を近似するための主力アルゴリズムです。行列に配置されたスナップショットを指定すると、次のようになります。

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 は、XAXX' \approx AX を使用して最適な線形演算子 AA を求めます。

  1. SVD を計算します: X=UΣVX = U \Sigma V^* 2.プロジェクト: A~=UXVΣ1\tilde{A} = U^* X' V \Sigma^{-1}
  2. 固有分解: A~W=WΛ\tilde{A} W = W \Lambda
  3. フルスペースモードを回復します: Φ=XVΣ1W\Phi = X' V \Sigma^{-1} W

Φ\Phi の列は DMD モード です。 Λ\Lambda の対角には 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

ここで意図的な選択が 2 つあります。セクション 4.2 ではウィンドウ全体でモード部分空間を比較し、正規化されていない振幅が比較に影響を与えるため、モードは L2 正規化されています。振幅は最初のスナップショットではなく最後のスナップショットに適合します。これは、予測が固有値を大きな累乗にすることは決してないことを意味します。これは、浮動小数点オーバーフローのときに単純な DMD 信号がダイナミクスのように見える数値爆発の原因です。

4. 測定

観測結果を状態空間構造に変換

この記事の教科書ではない部分です。上記はすべてフィッティング手順です。以下は、適合に意味があるかどうかを確認するためのプロトコルです。

データ。 株式 ETF の毎日のダウンロードではなく、プロジェクト独自の為替データを使用します。これは、一貫した分単位またはティック グリッド上の BTC、ETH、流動性アルトの断面です。このブログは暗号優先であり、マイクロ構造の議論は移転されません。行にアセット、列に時間を表示し、ログリターンに基づいて、各ウィンドウ内のアセットごとに評価された XX を構築します。

4.1 固有値スペクトル

代表的なウィンドウのレポート: 単位円の許容​​範囲内に収まる rr 固有値の数、それぞれの振動周期が 時間単位 (期間 =2π/ωj= 2\pi / |\omega_j| バー、換算) に対応する振動周期、および上位の rr モードが再構築するリターン分散の何分の 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 時間年中無休の暗号通貨には単純に引き継がれません。

4.2 隣接するウィンドウ間でのモードの安定性

決定的なテスト。 DMD をウィンドウ tt にフィットさせ、次にウィンドウ t+1t+1 にフィットさせ、ドミナント モード部分空間がどれだけ残っているかを測定します。正しい統計は、モード ベクトルの単純な相関ではなく、モードの順序と複素位相は任意です。2 つの部分空間間の主角です。

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

このオーバーラップの分布をレポートし、ヌルに対してレポートします。同じ統計は、同じリターンの位相ランダム化サロゲートで計算されます。オーバーラップが高いものの、サロゲート null より大きくない場合は、モードがダイナミクスではなく共分散構造を追跡していることを意味します。

4.3 現実のボラティリティに対するローリングスペクトル半径

テスト対象の主張: ローリング ウィンドウで再計算された ρt=maxjλj\rho_t = \max_j |\lambda_j| は、ボラティリティとともにではなく、ボラティリティが実現する「前」に移動します。スカラーは機械的に新しいものです。このブログでは以前に幾何学的スカラー、特に アルゴリズム取引のための複雑多様体] の小林曲率をリアルタイムで監視しましたが、コープマン固有値係数は異なる故障モードを持つ異なる量であり、継承されたピッチではなく独自のリードラグ テストに値します。

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 のピークは、ρt\rho_t が追加のステップを含むボラティリティの再計算であることを意味します。負のラグにおけるピークは、サンプル全体およびランク全体にわたって安定しており、この記事の中で取引可能な主張が含まれる唯一のバージョンです。

肯定的な結果の後に起こることについてのメモ

ρt\rho_t がリードする場合、明らかな次の動きはクロスセクショナルな構築です。DMD が予測する次のステップのリターンによって資産をランク付けし、予測された勝者をロングし、予測された敗者をショートします。この構造はここでは新しいものではありません。これは、暗号化における統計的裁定取引とペア取引] および ベクトルと行列を使用した複雑な裁定取引;唯一真に Koopman 特有の工夫は、固有ポートフォリオが静的な 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 は、まず非線形基底関数の辞書を通じてデータをリフトします。

スカラー関数 di:RnRd_i: \mathbb{R}^n \to \mathbb{R} の辞書 D={d1,,dp}\mathbf{D} = \{d_1, \ldots, d_p\} を指定して、リフトされた状態を定義します。

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 は、zk+1Kzkz_{k+1} \approx K z_k を使用して KRp×pK \in \mathbb{R}^{p \times p} をシークします。以下のコードで使用される規則では、columns として記述され、KK が左側で動作します。

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}

辞書の種類 機能 キャプチャ
多項式 xi, xixj, xi2,x_i,\ x_i x_j,\ x_i^2, \ldots 非線形の資産間相互作用
ラジアル基底 (RBF) exp(γxck2)\exp(-\gamma \lVert x - c_k \rVert^2) 局所的類似性、体制クラスタリング
時間遅延埋め込み xk, xk1,, xkτx_k,\ x_{k-1}, \ldots,\ x_{k-\tau} 記憶 / 自己回帰構造
フーリエ sin(2πfjt), cos(2πfjt)\sin(2\pi f_j t),\ \cos(2\pi f_j t) 既知の周期性 (日中、毎週)
ボラティリティの特徴 rt, rt2\lvert r_t \rvert,\ r_t^2 不均一分散性、ボリューム クラスタリング

辞書はドメインの知識が入る場所であり、時間遅延の行は、そもそも遅延座標を気にするコープマン特有の理由です。これらは追加された別個の技術ではなく、リフティング マップのもう 1 つのブロックです。埋め込み自体 (遅延 τ\tau、埋め込みディメンション dd、およびその背後にある再構成定理の選択) は、すでに導入され、コーディングされています アルゴリズム取引のための複雑な多様体 ;そこから遅延ベクトルを取得し、追加の行として 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. Deep Koopmanネットワーク

深層ネットワーク学習の潜在線形ダイナミクス

EDMD 辞書は手作りされており、クープマン不変部分空間が不明な場合、これが実際の制限となります。ディープ Koopman ネットワークはリフティングとオペレーターを共同で学習します。

このアーキテクチャはオートエンコーダです。エンコーダ、潜在コード、デコーダ、再構築損失はすべて algotrading における異常検出] で紹介されているものです。さらに、このセクションの要点である 1 つの追加が追加されています。それは、単一の学習済み行列 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}}

α\alpha 項がなければ、潜在空間の後にたまたま行列乗算が続く通常のオートエンコーダーになります。これにより、ネットワークは、その展開が「線形ではない」潜在表現に対してペナルティを課せられます。これにより、学習された KK がクープマン近似となり、その固有値がセクション 4 の DMD スペクトルに匹敵するようになります。

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 は、シーケンス モデルに到達するのではなく、これをトレーニングする価値があるものにします。学習された演算子はまだ行列であるため、セクション 4.2 の安定性テストとセクション 4.3 のリードラグ テストが変更されずに深いモデルに適用されます。

7. 実際的な考慮事項と落とし穴

モデルのドリフトとまばらな観測をナビゲートする

ランクの選択。 切り捨てランク rr はバイアス分散ダイヤルです。低すぎるとダイナミクスを失い、高すぎるとノイズに適合します。特異値エルボを注目しないでください。このブログでは、ベクトルと行列を使用した複雑なアービトラージ] のランダム行列理論からのマルチェンコ-パスツール拘束を使用して、「ノイズをフィッティングする前にコンポーネントの数」についてすでに適切に答えています。ウィンドウ形状のマルチェンコ-パストゥール エッジを超える特異値を持つコンポーネントを保持し、結果として得られる rr を、公開するすべてのスペクトルとともに明示的に記述します。

ウィンドウの長さ コープマン理論では、固定の FF を前提としています。市場はそれを供給しません。ローリング改修は必須であり、セクション 4.2 の安定曲線は、選択したウィンドウが推定するのに十分な長さであるか、1 つのレジーム内にとどまるのに十分短いかどうかを正確に診断します。

ノイズ感度 財務データの信号対ノイズ比は低く、標準 DMD は XX のノイズによってバイアスされます。モードが不安定であると結論付ける前に、試してみる価値のある解決策:

  • 合計 DMD (TDMD) — 合計最小二乗法により XXXX' の両方をノイズがあるものとして扱います。
  • 最適化された DMD — 残差フロベニウス ノルムに対して固有値モード分解を直接最適化します。
  • カーネル EDMD — 辞書を構築せずに、高次元の特徴空間で暗黙的に動作します。

TDMD の下でモードの安定性が大幅に向上する場合、その不安定性は測定ノイズでした。そうでない場合は、それは市場です。

8. DMD が存在する場所

モデリング アプローチ間の動的モード分解

方法 直線性 解釈可能 複数段階の予測
DMD 状態空間の線形 はい (モード + 固有値) 安定(マトリックスパワー)
EDMD 持ち上げられた空間での線形 写真はい、辞書を考慮すると 安定(マトリックスパワー)
ディープ・コープマン 学習空間における線形 中程度 (潜在的な K を検査) 安定(マトリックスパワー)

ボラティリティ固有の予測の場合、比較ポイントは GARCH ファミリーです。「crypto](/en/blog/post/garch-volatility-forecasting-crypto) の GARCH ボラティリティ予測」を参照してください。完全に非線形のシーケンス モデルと、それらがもたらす解釈可能性と誤差累積のトレードオフについては、trading の Temporal Fusion Transformer を参照してください。

DMD が占めるニッチな領域は狭いですが現実的です。つまり、自己回帰ロールアウトではなく単一の行列乗によって生成されるマルチステップ予測であり、すべてのモードが検査可能です。そのニッチにアルファが含まれるかどうかはセクション 4 の問題であり、この表の問題ではありません。

## 結論

乱流の軌道が安定モードに解決

コープマン理論は、市場のダイナミクスを観察するための真にエレガントな方法であり、エレガンスだからこそ、利用可能な最も厳しいテストが必要です。要点:

  1. DMD をフィットし、すぐにフィットをテストします。 位相ランダム化ヌルに対して測定された、隣接するウィンドウ間の部分空間の重なりにより、構造を見つけたかウィンドウを記憶したかが午後以内にわかります。
  2. ローリングスペクトル半径は監視する価値のある 1 つのスカラーです。その値は完全に進み遅れの結果に依存します。ラグ 0 では、ボラティリティ プロキシになります。負の遅れでは、それは政権の警告です。
  3. 最後のスナップショットで振幅を固定し、固有値をウィンドウの長さまで上げないでください。世の中の DMD 「信号」の大部分は浮動小数点アーティファクトです。
  4. 肘を目視するのではなく、マルチェンコ-パストゥールによってランクを選択し、あらゆるスペクトルでランクを公開します。
  5. スペクトル プロットは結果ではありません。 これに基づいて構築された戦略は、手数料、スリッページ、およびデフレート シャープ テストに耐えなければ意味がありません。

これらを超える実装については、PyDMD ライブラリが DMD バリアントを包括的にカバーしており、Mallen らのリファレンス コードが Koopman 側の詳細をカバーしています。

市場は混乱し、非定常で、部分的に観測される状態が続くだろう。コープマン理論は、その混乱から構造を抽出するための原則に基づいたレンズを提供します。ただし、来週もその構造がまだ存在していることを確認できます。


参考文献と詳細情報:

  • B. O. Koopman、「ヒルベルト空間におけるハミルトニアン システムと変換」米国科学アカデミー紀要、1931 年。
  • J. H. Tu 他、「動的モード分解について: 理論と応用」、Journal of Computational Dynamics、2014 年。
  • M. O. Williams、I. G. Kevrekidis、C. W. Rowley、「クープマン演算子のデータ駆動近似: 動的モード分解の拡張」Journal of Nonlinear Science、2015 年。
  • B. Lusch、J. N. Kutz、S. L. Brunton、「非線形ダイナミクスの普遍的な線形埋め込みのための深層学習」Nature Communications、2018 年。
  • J. Mann および J. N. Kutz、「金融取引戦略のための動的モード分解」Quantitative Finance、2016 年。
  • A. Mallen 他、「時間分布シフトを伴う時系列用の Koopman Neural Forecaster」、ICML、2023 年。
  • E. Gonzalez および M. Generelo、「コープマン演算子、EDMD、テインズの定理、および機械学習によるカオス経済モデルの分析」金融と経済におけるデータ サイエンス、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

市場の先を行く

ニュースレターを購読して、独占的なAI取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。