Neural ODEs: Does Continuous Time Beat a Delta-Time Feature?
神經常微分方程(Chen 等人,NeurIPS 2018)用神經網路參數化隱藏狀態的導數,再由 ODE 求解器計算輸出。這讓模型具有連續深度,更重要的是對市場具有連續時間:隱藏狀態可在任意 評估,而不只是在資料恰好落點的網格上。
這項特性對以隨機間隔到達的金融資料很有吸引力。但這個宣稱背後的主張其實弱得多。只要給普通 GRU 一個額外輸入特徵 - 上次觀察以來的 - 它同樣「知道」經過了多久。真正的問題不是連續時間模型能否讀取不規則資料,而是觀察之間學到的連續動態,是否能提取時間差特徵已經沒有提供的資訊;代價卻是十到一百倍的推理成本。
本文設定這項實驗,提供執行它所需的完整工具鏈,並說明比較結果必須展示什麼,才能證明這套機制值得使用。關於柱線構造的論點 - 交易時機本身帶有訊號 - 已由超越時間柱線中對 17 種柱線類型的測量證據建立。這裡唯一重要的建模後果是:求解器能在任意 評估隱藏狀態,因此不需要插值或填充。
待檢驗的主張

三個巢狀假設,每一個都嚴格強於前一個:
- H1 - 在原始不規則 BTC 交易流上訓練的 ODE-RNN,勝過同一交易流上完全沒有時間資訊的 GRU。這幾乎肯定成立,但幾乎沒有價值:ODE-RNN 只是擁有基線缺少的資訊。
- H2 - 當 GRU 得到 作為輸入特徵後,ODE-RNN 仍勝過同一個 GRU。這才是誠實的測試,也是神經 ODE 文獻通常被解讀為提出的主張,但在金融資料上幾乎從未得到證明。
- H3 - H2 的優勢能在延遲預算內保留:ODE-RNN 不用自適應的
dopri5,而改用足夠快、可即時推理的固定步長求解器時,優勢仍然存在。
實驗協議
兩個實驗臂使用相同的 tick 流、相同的目標和相同的調參預算。不重採樣為 1 分鐘柱線 - 重採樣會破壞正在檢驗的精確結構。
| 設定 | |
|---|---|
| 資料 | BTC 成交列印,可變的成交間隔,未重採樣 |
| 實驗臂 | ODE-RNN;GRU + ;不含時間特徵的 GRU |
| 目標 | 所有實驗臂相同;隨擬合一併指定 |
| 指標 | RMSE、留出對數似然、每 epoch 掛鐘時間、每步推理延遲 |
| 求解器掃描 | dopri5(rtol 1e-5)與固定步長 Euler,並匹配訓練精度 |
| 切分 | Walk-forward,僅使用樣本外資料 |
| 實驗臂 | RMSE | 對數似然 | 秒/epoch | 每步推理延遲 |
|---|---|---|---|---|
| GRU(無時間特徵) | — | — | — | — |
| GRU + log(dt) | — | — | — | — |
| ODE-RNN(dopri5) | — | — | — | — |
| ODE-RNN(固定步長 Euler) | — | — | — | — |
H2 的負面結果完全可以發表,而且可能是更有價值的結果 - 誠實的負面結果與多重測試下的折減夏普採用的正是同一標準。
第二項實驗:CNF 與 Student-t
如果 ODE-RNN 比較的成本太高,較便宜的經驗錨點是分佈建模:在真實 BTC 日收益上擬合連續標準化流,將擬合密度與 Student-t 擬合及經驗尾部比較,並報告 1% 和 5% 水平的尾部分位數誤差。這直接連接到GARCH 波動率預測與非對稱 GARCH中已測量的分佈工作。
本文其餘部分介紹執行上述實驗所需的背景和工具鏈。
背景:從 ResNet 到連續動力學

殘差網路計算 。縮小步長並增加層數,就會逼近連續極限:
與具有獨立參數的 個離散層不同,單一網路 指定瞬時變化率。 時刻的輸出解決初值問題:
黑盒求解器(Euler、Runge-Kutta、Dormand-Prince)以數值方式計算積分,並根據局部誤差估計自適應選擇步長。
伴隨方法
透過每個求解器步驟反向傳播,會儲存所有中間狀態,記憶體消耗與步數成正比。Chen 等人改為用 記憶體求解反向 ODE。定義伴隨狀態 ,它滿足
並在反向過程中累積參數梯度:
反向傳播同時求解計算 、 和 的增強系統,讓求解器從 反向運行到 。
取捨是:反向重建 會累積數值誤差,對剛性或混沌動力學尤其嚴重。檢查點是折衷方案 - 在幾個中間時刻儲存 ,再在其間重新計算。價格過程是連續半鞅且相當平滑,因此伴隨方法通常可行;但微觀結構事件附近的剛性,可能迫使我們使用自適應求解器或混合求解器。
ODE-RNN:觀察之間的連續隱藏狀態

ODE-RNN 是主要實驗使用的架構。在觀察之間,隱藏狀態依 ODE 演化:
當觀察 在 到達時,觸發離散更新:
上標表示觀察前後的狀態。觀察之間學習連續動力學;觀察到達時接收新資訊。
H2 消融檢驗的機制是:長時間間隔意味著隱藏狀態在 下演化了很長一段路 - 向基線衰減,或向外發散 - 而演化形狀是學到的,不是作為一個純量提供。這種表現力是否能在真實交易資料上帶來回報,正是尚未測量的問題。
它自然適合超越 tick 的場景:多資產投資組合中,每種資產都有自己的觀察排程,相關資產的潛在狀態持續演化,但一次只觀察到其中一種;以及不規則到達的事件驅動訊號(新聞、業績、宏觀資料發布)。
神經 SDE:加入隨機成分

神經 ODE 是確定性的,無法表示價格路徑中的雜訊。Black-Scholes 選擇權定價介紹了經典處理方式 - 幾何布朗運動、風險中性漂移,以及常數波動率假設為何無法解釋波動率微笑。神經 SDE 用學到的擴散項取代參數化形式:
是漂移, 是擴散, 是 Wiener 過程,而 與 都是神經網路。
架構:漂移網路與擴散網路
- 漂移網路 :預期軌跡 - 趨勢、均值回歸、動量。訓練目標是最小化預測誤差。
- 擴散網路 :作為狀態函數學習的雜訊幅度,在波動 regime 中較高,在平靜 regime 中較低。
這種拆分反映了經典量化金融:漂移是風險中性(或 P 測度)動力學,擴散是波動率曲面。
訓練神經 SDE
隨機積分 在經典意義下不可微。可用三種方法:
- 路徑梯度:重參數化技巧 - 抽樣布朗路徑,將雜訊視為固定輸入,對穿過求解器的路徑求導。
- 分數匹配:估計 ,透過去噪目標訓練 - 與加密貨幣預測的擴散模型中作為獨立生成模型使用的機制相同,只是在此降為 SDE 的訓練選項。
- 有限維分佈匹配:在觀察時刻匹配邊際分佈,而不是完整路徑測度。
路徑梯度是實務上的預設選擇。torchsde 實作 Euler-Maruyama、Milstein 和隨機 Runge-Kutta,並支援自動微分。
學習波動率曲面
經典模型(Heston、SABR)為擴散係數設定參數化形式。神經 SDE 將 學習為任意函數:
這是擴散過程的通用逼近器:只要容量足夠,任何 Ito 過程都能達到任意精度。
缺失與非同步資料的潛在 ODE

潛在 ODE(Rubanova、Chen、Duvenaud,2019)將神經 ODE 與 VAE 配對:金融序列是低維潛在空間中學到的平滑底層過程之雜訊觀察。
- 辨識網路:ODE-RNN 沿觀察序列向後運行,以產生 。
- 潛在動力學:。
- 解碼器:,可在任意指定時刻評估。
損失是標準 ELBO:
投資組合狀態估計:從跨資產的稀疏非同步觀察中推斷捕捉聯合動力學的連續潛在狀態,本質上是卡爾曼濾波器的非線性學習版本。
缺失資料插補:停牌與週末間隔得到平滑插值的潛在軌跡;解碼器生成穿過間隔的合理路徑,並保留來自 VAE 後驗的不確定性。
多頻率融合:在同一模型中處理每日收盤價、日內 VWAP 和 tick 資料,不需要共用時間網格。
報酬分佈的連續標準化流

CNF 使用神經 ODE 將簡單的基礎分佈轉換為複雜的目標分佈。變換為 ,對數密度遵循瞬時變數變換:
從 向前積分狀態和對數密度,得到 與 。為了可擴展性,雅可比跡使用 Hutchinson 隨機估計器估計。
加密貨幣收益具有尖峰厚尾且負偏 - GARCH 波動率預測與非對稱 GARCH 及槓桿效應已在真實資料上測量這一點 - CNF 會學習這種形狀,而不是先驗假設它。以狀態變數作為流的條件,可以讓形狀隨 regime 改變。由於密度是精確而非近似的,它可以餵給蒙地卡羅與 bootstrap 回測計算的尾部指標,以及HRP/CVaR 投資組合流程中的 CVaR 構造;聯合尾部結構則在聯合風險的 copula 模型中另行處理。
條件密度估計
有用的比較方式,是把同一問題的三種已發表方法直接放在一起。TFT透過分位數輸出層給出固定的一組分位數。保序預測給出具有覆蓋率保證的校準區間。條件 CNF 則給出精確且可微的密度:
可微性是它的區別所在:密度可以放進下游目標並透過反向傳播,而固定分位數和保序區間都不支援這一點。在相同目標上,精確密度是否值得相對於 TFT 分位數付出額外成本,本文尚未測試。
與離散替代方案的比較

| 屬性 | LSTM/GRU | Transformer | Neural ODE | Neural SDE |
|---|---|---|---|---|
| 不規則時間處理 | 較差(需要填充) | 位置編碼 | 原生支援 | 原生支援 |
| 記憶體(訓練) | 伴隨法 | 伴隨法 | ||
| 不確定性量化 | 否(確定性) | 否(確定性) | 透過 ensemble | 原生支援 |
| 觀察之間的插值 | 否 | 否 | 是 | 是 |
| 連續時間密度 | 否 | 否 | 透過 CNF | 透過路徑測度 |
| 計算成本 | 低 | 中等 | 可變(取決於求解器) | 高(SDE 求解器) |
這張表中 LSTM 與 attention 的部分,已在TFT 文章中配合基準測試詳細論證,請參閱其中的「TFT vs LSTM vs Vanilla Transformer」與「When LSTM Still Wins」章節。這裡新增的是右側兩欄,而決定 H2/H3 問題的則是最後兩列。
使用 torchdiffeq 的 Python 實作

torchdiffeq 提供帶有伴隨反向傳播的 ODE 求解器。
價格動力學的基本神經 ODE
import torch
import torch.nn as nn
from torchdiffeq import odeint_adjoint as odeint
class PriceDynamics(nn.Module):
"""Neural network defining dh/dt = f(h, t)."""
def __init__(self, hidden_dim: int = 64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(hidden_dim, 128),
nn.Tanh(),
nn.Linear(128, 128),
nn.Tanh(),
nn.Linear(128, hidden_dim),
)
def forward(self, t, h):
return self.net(h)
class NeuralODEPredictor(nn.Module):
"""
Encode observed features -> latent state,
evolve via Neural ODE,
decode to price prediction.
"""
def __init__(self, input_dim: int, hidden_dim: int = 64):
super().__init__()
self.encoder = nn.Linear(input_dim, hidden_dim)
self.dynamics = PriceDynamics(hidden_dim)
self.decoder = nn.Linear(hidden_dim, 1)
def forward(self, x0, eval_times):
"""
x0: (batch, input_dim) features at t=0
eval_times: (T,) times at which to evaluate the ODE
Returns: (T, batch, 1) predictions
"""
h0 = self.encoder(x0) # (batch, hidden_dim)
h_traj = odeint(self.dynamics, h0, eval_times,
method='dopri5', rtol=1e-5, atol=1e-7)
return self.decoder(h_traj)
不規則 tick 資料的 ODE-RNN
這是實驗臂。它必須擊敗的基線,是在同一資料流上將 log(t_next - t_prev) 拼接到 x 的 nn.GRUCell。
class ODERNNCell(nn.Module):
"""Single step: ODE-evolve, then RNN-update."""
def __init__(self, input_dim: int, hidden_dim: int = 64):
super().__init__()
self.dynamics = PriceDynamics(hidden_dim)
self.gru_cell = nn.GRUCell(input_dim, hidden_dim)
def forward(self, h, x, t_prev, t_next):
times = torch.tensor([t_prev, t_next], dtype=torch.float32)
h_evolved = odeint(self.dynamics, h, times,
method='dopri5')[-1] # state at t_next
h_updated = self.gru_cell(x, h_evolved)
return h_updated
class ODERNN(nn.Module):
"""Process irregularly-sampled sequence."""
def __init__(self, input_dim: int, hidden_dim: int = 64):
super().__init__()
self.cell = ODERNNCell(input_dim, hidden_dim)
self.decoder = nn.Linear(hidden_dim, 1)
self.hidden_dim = hidden_dim
def forward(self, observations, times):
"""
observations: list of (batch, input_dim) tensors
times: list of floats, observation timestamps
"""
batch_size = observations[0].shape[0]
h = torch.zeros(batch_size, self.hidden_dim)
outputs = []
for i in range(len(observations)):
t_prev = 0.0 if i == 0 else times[i - 1]
h = self.cell(h, observations[i], t_prev, times[i])
outputs.append(self.decoder(h))
return torch.stack(outputs) # (seq_len, batch, 1)
訓練迴圈
def train_neural_ode(model, train_loader, epochs=100, lr=1e-3):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=epochs
)
for epoch in range(epochs):
epoch_loss = 0.0
for batch in train_loader:
features, times, targets = batch
optimizer.zero_grad()
predictions = model(features, times)
loss = torch.nn.functional.mse_loss(predictions, targets)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
scheduler.step()
if (epoch + 1) % 10 == 0:
avg_loss = epoch_loss / len(train_loader)
print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.6f}")
收益分佈的連續標準化流
from torchdiffeq import odeint
class CNFDynamics(nn.Module):
"""Dynamics for continuous normalizing flow."""
def __init__(self, dim: int = 1, hidden_dim: int = 64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim + 1, hidden_dim), # +1 for time
nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, dim),
)
self.dim = dim
def forward(self, t, state):
z = state[..., :self.dim]
t_expand = t.expand(z.shape[0], 1)
zt = torch.cat([z, t_expand], dim=-1)
dz = self.net(zt)
e = torch.randn_like(z)
e_dz = torch.autograd.grad(
dz, z, e, create_graph=True
)[0]
trace_jac = (e_dz * e).sum(dim=-1, keepdim=True)
return torch.cat([dz, -trace_jac], dim=-1)
class ReturnDistributionCNF(nn.Module):
"""Model return distributions with continuous normalizing flows."""
def __init__(self, dim: int = 1):
super().__init__()
self.dynamics = CNFDynamics(dim)
self.dim = dim
def log_prob(self, x):
"""Compute log probability of observed returns."""
log_p0 = torch.zeros(x.shape[0], 1)
state0 = torch.cat([x, log_p0], dim=-1)
state0.requires_grad_(True)
times = torch.tensor([1.0, 0.0]) # backward
state_T = odeint(self.dynamics, state0, times,
method='dopri5')[-1]
z_T = state_T[..., :self.dim]
delta_log_p = state_T[..., self.dim:]
log_p_base = -0.5 * (z_T ** 2 + torch.log(
torch.tensor(2 * torch.pi)
)).sum(dim=-1, keepdim=True)
return log_p_base + delta_log_p
def sample(self, n_samples: int):
"""Generate samples from learned distribution."""
z0 = torch.randn(n_samples, self.dim)
log_p0 = torch.zeros(n_samples, 1)
state0 = torch.cat([z0, log_p0], dim=-1)
times = torch.tensor([0.0, 1.0]) # forward
state_T = odeint(self.dynamics, state0, times,
method='dopri5')[-1]
return state_T[..., :self.dim]
實務注意事項

以下是執行上述實驗時最容易踩到的問題。
求解器選擇與速度
dopri5 提供精度保證,但計算成本可變,因此 H3 必須與 H2 分開:只在自適應求解器下存在的優勢,可能無法通過實盤延遲預算。固定步長求解器(Euler、RK4)以精度為代價提供可預測延遲。實務折衷是用 dopri5 訓練,部署時使用在代表性資料上校準、能匹配自適應求解器輸出的固定步長求解器 - 測量差距,不要假定差距很小。
odeint(f, h0, t, method='dopri5', rtol=1e-6, atol=1e-8)
odeint(f, h0, t, method='euler', options={'step_size': 0.1})
對於不連續跳躍附近的剛性問題,可使用 method='implicit_adams' 或 method='scipy_solver'。
Numerical Stability
如果 輸出很大的值,狀態會發散。可採取以下緩解措施:
- 在 層上使用譜歸一化,控制 Lipschitz 常數。
- 訓練期間梯度裁剪(如上方訓練迴圈所示)。
- 時間歸一化:將時間戳縮放到 。
- 動力學範數正則化:將 加入損失。
積分區間
對於跨越數月的資料,不要從 積分到 分鐘:
t_normalized = (timestamps - timestamps[0]) / (timestamps[-1] - timestamps[0])
這能讓求解器保持在數值上友善的區間,並讓學到的動力學具備尺度不變性。這對比較也很重要:未歸一化的 ODE-RNN 可能只因數值原因輸給 GRU 基線,那會是測量偽象,而不是研究發現。
處理多重時間尺度
市場同時具有微秒、秒、分鐘和日級動力學,單一神經 ODE 可能難以容納全部。可選方案包括:在不同時間尺度堆疊多個 ODE 區塊;擴大隱藏維度以同時容納快、慢動力學;或對每個頻段執行獨立的神經 ODE,再融合輸出。(這是模型容量問題,不同於自適應解析度深入分析中的回測保真度問題。)
開放研究方向

神經跳躍 SDE:為突然的市場斷裂(閃崩、業績意外)加入學習型跳躍成分:
其中 是補償泊松隨機測度, 是學習到的跳躍核。
神經控制微分方程(神經 CDE):以一般驅動訊號取代 Wiener 過程,讓觀察到的資料流驅動動力學。這很適合訂單流,因為交易與報價流會驅動潛在市場狀態。
可微市場模擬:在可微模擬器中使用神經 SDE 作為生成模型,透過伴隨方法反向傳播,端到端訓練策略。策略與市場模型共同演化。
物理資訊神經常微分方程:PINN 將微分方程殘差嵌入損失 - 這項技術本身在Navier-Stokes 文章中介紹。金融應用是將無套利、買賣權平價和鞅條件作為懲罰項,或作為學習動力學的硬約束。
結論

連續時間框架在結構上是正確的:市場是連續過程,只是在離散且不規則的時刻被觀察,模型應該尊重這一點。工具鏈已經成熟 - torchdiffeq、torchsde,以及其餘部分的原生 PyTorch - 已知成本則是求解器速度,以及長積分區間內的數值穩定性。
尚未建立的是決定它們是否應進入生產堆疊的關鍵部分。結構正確不等於具有預測優勢,而連續動力學相對於 特徵所宣稱的具體優勢,尚未在本文的真實交易資料上測量。在上方 H2 表格填滿之前,請把折線以下的所有內容視為附有可執行實作的明確假設,而不是結果。
參考文獻
- Chen, R.T.Q.、Rubanova, Y.、Bettencourt, J.、Duvenaud, D. (2018)。神經常微分方程。NeurIPS 2018。arXiv:1806.07366
- Rubanova, Y.、Chen, R.T.Q.、Duvenaud, D. (2019)。不規則採樣時間序列的潛在常微分方程。NeurIPS 2019。arXiv:1907.03907
- Jia, J.、Benson, A.R. (2019)。神經跳躍隨機微分方程。NeurIPS 2019。
- Kidger, P.、Morrill, J.、Foster, J.、Lyons, T. (2020)。不規則時間序列的神經控制微分方程。NeurIPS 2020。
- Hasan, A.、Pereira, J.M.、Farsiu, S.、Carin, L. (2021)。神經網路隨機微分方程模型及其在金融資料預測的應用。arXiv:2111.13164
- torchdiffeq:github.com/rtqichen/torchdiffeq
- UvA 深度學習教程 - 神經 ODE:uvadlc-notebooks.readthedocs.io
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.