Neural ODEs: Does Continuous Time Beat a Delta-Time Feature?
Neural Ordinary Differential Equations (Chen et al., NeurIPS 2018) tham số hóa đạo hàm của một trạng thái ẩn bằng mạng nơ-ron và để bộ giải ODE tính output. Model trở thành continuous-depth và, hữu ích hơn với thị trường, continuous-time: trạng thái ẩn có thể được đánh giá tại mọi , không chỉ tại các điểm lưới nơi dữ liệu tình cờ nằm trên đó.
Tính chất này có một lời chào hàng hiển nhiên cho dữ liệu tài chính, vốn đến ở các khoảng thời gian ngẫu nhiên. Nhưng lời chào hàng che giấu một claim yếu hơn nhiều so với vẻ ngoài. Một GRU đơn giản được đưa thêm một feature — kể từ observation trước — cũng "biết" bao nhiêu thời gian đã trôi qua. Câu hỏi thực sự không phải là model continuous-time có thể nhận dữ liệu không đều hay không. Mà là liệu động lực học liên tục được học giữa các observation có trích xuất được điều gì mà feature delta-time chưa cung cấp, với chi phí inference cao gấp mười đến một trăm lần hay không.
Bài viết này thiết lập experiment đó, cung cấp toàn bộ toolchain để chạy và nêu rõ comparison phải cho thấy điều gì để biện minh cho machinery. Phần lập luận về dựng bar — rằng bản thân thời điểm giao dịch mang tín hiệu — đã được xác lập bằng bằng chứng đo trên 17 loại bar trong beyond time bars; hệ quả duy nhất quan trọng ở đây là về modeling: solver đánh giá trạng thái ẩn tại bất kỳ, nên không cần interpolation hay padding.
Tuyên bố được kiểm tra

Ba giả thuyết lồng nhau, mỗi giả thuyết mạnh hơn giả thuyết trước:
- H1 — ODE-RNN fit trên raw irregular BTC trade stream đánh bại GRU fit trên cùng stream nhưng hoàn toàn không có time information. Điều này gần như chắc chắn và gần như vô nghĩa: ODE-RNN đơn giản có information mà baseline thiếu.
- H2 — ODE-RNN vẫn đánh bại cùng GRU sau khi GRU được cho làm input feature. Đây là test trung thực. Đây là claim mà literature về Neural ODE thường được hiểu là đưa ra, và nó gần như chưa bao giờ được chứng minh trên dữ liệu tài chính.
- H3 — Lợi thế H2 tồn tại sau latency budget, tức là vẫn giữ khi ODE-RNN được triển khai với fixed-step solver đủ nhanh cho live inference thay vì adaptive
dopri5.
Giao thức thí nghiệm
Cùng tick stream, cùng target, cùng tuning budget cho cả hai arm. Không resample thành bar 1m — resampling phá hủy chính cấu trúc đang được kiểm tra.
| Cấu hình | |
|---|---|
| Dữ liệu | BTC trade prints, khoảng cách giữa các event thay đổi, không resample |
| Nhánh thử nghiệm | ODE-RNN; GRU + ; GRU không có time feature |
| Target | Giống nhau cho mọi nhánh; được nêu cùng phép fit |
| Chỉ số | RMSE, held-out log-likelihood, wall-clock mỗi epoch, inference latency mỗi step |
| Quét solver | dopri5 (rtol 1e-5) so với fixed-step Euler, ghép theo training accuracy |
| Phân chia | Walk-forward, chỉ out-of-sample |
| Nhánh | RMSE | Log-lik | s/epoch | Inference latency/step |
|---|---|---|---|---|
| GRU (no time feature) | — | — | — | — |
| GRU + log(dt) | — | — | — | — |
| ODE-RNN (dopri5) | — | — | — | — |
| ODE-RNN (fixed-step Euler) | — | — | — | — |
Kết quả âm của H2 hoàn toàn có thể công bố và có lẽ còn giá trị hơn — cùng tiêu chuẩn đã áp dụng trong kết quả phủ định trung thực và deflated Sharpe dưới multiple testing.
Thí nghiệm phụ: CNF so với Student-t
Nếu comparison ODE-RNN quá tốn kém, anchor thực nghiệm rẻ hơn là phân phối: fit continuous normalizing flow trên daily return BTC thực và so sánh density đã fit với fit Student-t và với empirical tail, báo cáo sai số tail-quantile ở mức 1% và 5%. Điều này kết nối trực tiếp với công việc phân phối đã được đo trong GARCH volatility forecasting và asymmetric GARCH.
Phần còn lại của bài viết là background và toolchain cần thiết để chạy experiment trên.
Bối cảnh: Từ ResNet đến động lực học liên tục

Một residual network tính . Giảm step size và tăng số layer, ta tiến tới một giới hạn liên tục:
Thay vì layer rời rạc với các parameter riêng, một network duy nhất xác định tốc độ thay đổi tức thời. Output tại thời điểm giải một bài toán giá trị ban đầu:
Một black-box solver (Euler, Runge-Kutta, Dormand-Prince) tính tích phân bằng số, tự động chọn step size từ các ước lượng local error.
Phương pháp adjoint
Backpropagate qua mọi solver step cần lưu tất cả trạng thái trung gian, tốn memory tỷ lệ với số step. Thay vào đó, Chen et al. giải một ODE ngược với memory . Định nghĩa trạng thái adjoint , thỏa mãn
và tích lũy gradient của parameter trong backward pass:
Backward pass giải một hệ augmented để tính đồng thời , và , chạy solver ngược từ về .
Trade-off là: tái tạo theo chiều ngược tích lũy numerical error, đặc biệt nghiêm trọng với động lực học stiff hoặc chaotic. Checkpointing là điểm giữa — lưu tại một số thời điểm trung gian và tính lại giữa chúng. Price process là continuous semimartingale và khá smooth, nên adjoint thường hoạt động tốt; stiffness quanh microstructure event có thể buộc dùng adaptive solver hoặc hybrid.
ODE-RNN: Trạng thái ẩn liên tục giữa các observation

ODE-RNN là architecture mà experiment chính xoay quanh. Giữa các observation, trạng thái ẩn tiến hóa theo ODE:
Khi observation đến tại , một discrete update được kích hoạt:
Các superscript biểu thị trạng thái ngay trước và ngay sau observation. Giữa các observation là động lực học liên tục được học; tại observation là information mới.
Cơ chế mà H2 ablation thăm dò là: một khoảng gap dài nghĩa là trạng thái ẩn đã tiến hóa một quãng dài dưới — suy giảm về baseline hoặc phân kỳ — và hình dạng của tiến hóa đó được học thay vì được cung cấp như một scalar. Liệu tính biểu đạt đó có đáng chi phí trên trade data thực hay không chính là điều chưa được đo.
Các fit tự nhiên ngoài tick: portfolio đa tài sản nơi mỗi asset có observation schedule riêng và latent state của các asset tương quan tiếp tục tiến hóa trong khi chỉ một asset được quan sát; và các signal hướng event (news, earnings, macro release) đến ở thời điểm không đều.
Neural SDE: Thêm thành phần ngẫu nhiên

Neural ODE là deterministic và không thể biểu diễn noise trong price path. Cách xử lý cổ điển — geometric Brownian motion, risk-neutral drift và các cách giả định constant-volatility thất bại trước smile — được trình bày trong Black-Scholes options pricing. Neural SDE thay thế dạng parametric bằng một diffusion term được học:
là drift, là diffusion, là Wiener process; cả và đều là neural network.
Kiến trúc: Drift Net và Diffusion Net
- Drift net : quỹ đạo kỳ vọng — trend, mean reversion, momentum. Được training để giảm prediction error.
- Diffusion net : độ lớn noise, được học như một hàm của state. Cao trong regime biến động, thấp trong regime yên ắng.
Sự phân tách phản ánh quant finance cổ điển: drift là động lực học risk-neutral (hoặc P-measure), diffusion là volatility surface.
Huấn luyện Neural SDE
Stochastic integral không khả vi theo nghĩa cổ điển. Có ba cách tiếp cận:
- Pathwise gradients: reparameterization trick — sample các Brownian path, đạo hàm qua solver với noise được coi là input cố định.
- Score matching: ước lượng và training qua denoising objective — cùng machinery được dùng như một generative model độc lập trong diffusion model cho dự báo crypto, ở đây được hạ xuống thành một lựa chọn training cho SDE.
- Finite-dimensional distribution matching: match marginal tại thời điểm observation thay vì toàn bộ path measure.
Pathwise là mặc định thực tế. torchsde triển khai Euler-Maruyama, Milstein và stochastic Runge-Kutta với autodiff support.
Học volatility surface
Các model cổ điển (Heston, SABR) áp đặt dạng parametric lên diffusion coefficient. Neural SDE học như một hàm tùy ý:
Đây là universal approximator cho diffusion process — với đủ capacity, mọi Ito process đều có thể đạt accuracy tùy ý.
Latent ODE cho dữ liệu thiếu và bất đồng bộ

Latent ODE (Rubanova, Chen, Duvenaud, 2019) kết hợp Neural ODE với VAE: các chuỗi tài chính là observation nhiễu của một process nền trơn, được học trong latent space ít chiều.
- Recognition network: ODE-RNN chạy ngược qua các observation để tạo .
- Latent dynamics: .
- Decoder: , có thể đánh giá tại bất kỳ thời điểm yêu cầu nào.
Loss là ELBO tiêu chuẩn:
Ước lượng portfolio state: từ các observation thưa và bất đồng bộ trên nhiều asset, suy ra latent state liên tục nắm bắt joint dynamics — về cơ bản là một phiên bản phi tuyến, được học của Kalman filter.
Imputation dữ liệu thiếu: halt và khoảng trống cuối tuần nhận một latent trajectory được nội suy trơn; decoder tạo các path hợp lý đi qua khoảng trống với uncertainty từ posterior của VAE.
Fusion đa tần số: daily close, intraday VWAP và tick data trong một model không cần shared time grid.
Continuous Normalizing Flow cho phân phối return

CNF dùng Neural ODE để biến đổi một base distribution đơn giản thành target phức tạp. Phép biến đổi là , còn log-density tuân theo instantaneous change of variables:
Tích phân state và log-density về phía trước từ để nhận và . Trace của Jacobian được ước lượng bằng stochastic estimator của Hutchinson để mở rộng quy mô.
Crypto return có leptokurtic và negative skew — được đo trên dữ liệu thực trong GARCH volatility forecasting và asymmetric GARCH và leverage effect — và CNF học hình dạng đó thay vì giả định. Condition flow trên state variable cho phép hình dạng thay đổi theo regime. Vì density là exact thay vì approximate, nó có thể cấp cho tail metric được tính trong Monte Carlo và bootstrap backtest và construction CVaR trong pipeline portfolio HRP/CVaR; joint tail structure được xử lý riêng trong copula model cho joint risk.
Ước lượng density có điều kiện
Cách framing hữu ích là contrast trực tiếp giữa ba approach đã công bố cho cùng một bài toán. TFT cung cấp một tập quantile cố định từ quantile output layer. Conformal prediction cung cấp interval đã calibration với coverage guarantee. Conditional CNF cung cấp một density exact, khả vi:
Khả vi là tính chất phân biệt: density có thể nằm trong downstream objective và backpropagate qua đó, điều mà fixed quantile và conformal interval không hỗ trợ. Density exact có đáng chi phí so với TFT quantile trên cùng target hay không vẫn chưa được kiểm tra ở đây.
So sánh với các lựa chọn rời rạc

| Thuộc tính | LSTM/GRU | Transformer | Neural ODE | Neural SDE |
|---|---|---|---|---|
| Xử lý thời gian không đều | Kém (cần padding) | Positional encoding | Native | Native |
| Memory (training) | adjoint | adjoint | ||
| Định lượng uncertainty | Không (deterministic) | Không (deterministic) | Qua ensemble | Native |
| Nội suy giữa các observation | Không | Không | Có | Có |
| Density thời gian liên tục | Không | Không | Qua CNF | Qua path measure |
| Chi phí tính toán | Thấp | Vừa | Thay đổi (solver) | Cao (SDE solver) |
Nửa LSTM-so-với-attention của bảng này được lập luận chi tiết, cùng benchmark, trong bài TFT — xem các section "TFT vs LSTM vs Vanilla Transformer" và "When LSTM Still Wins". Các cột mới ở đây là hai cột bên phải, còn các dòng quyết định câu hỏi H2/H3 là hai dòng cuối.
Triển khai Python với torchdiffeq

torchdiffeq cung cấp ODE solver với adjoint backpropagation.
Neural ODE cơ bản cho price dynamics
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)
ODE-RNN cho dữ liệu tick không đều
Đây là experimental arm. Baseline phải đánh bại là nn.GRUCell trên cùng stream với log(t_next - t_prev) được nối vào x.
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)
Vòng lặp huấn luyện
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}")
Continuous Normalizing Flow cho phân phối return
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]
Ghi chú thực tế

Đây là những điều sẽ gây rắc rối khi chạy experiment ở trên.
Lựa chọn solver và tốc độ
dopri5 cung cấp accuracy guarantee nhưng compute cost thay đổi, đó là lý do H3 là hypothesis riêng với H2: một edge chỉ tồn tại dưới adaptive solver có thể không sống sót trong live latency budget. Fixed-step solver (Euler, RK4) cho latency dự đoán được với cái giá là accuracy. Thỏa hiệp thực tế: train bằng dopri5, deploy bằng fixed-step solver đã được calibration để output khớp adaptive solver trên dữ liệu đại diện — và đo gap thay vì giả định nó nhỏ.
odeint(f, h0, t, method='dopri5', rtol=1e-6, atol=1e-8)
odeint(f, h0, t, method='euler', options={'step_size': 0.1})
Với các bài toán stiff gần bước nhảy discontinuous, dùng method='implicit_adams' hoặc method='scipy_solver'.
Ổn định số
Nếu output các giá trị lớn, state sẽ diverge. Các biện pháp:
- Spectral normalization trên các layer của để kiểm soát Lipschitz constant.
- Gradient clipping khi training (đã được minh họa trong training loop ở trên).
- Time normalization: scale timestamp về .
- Regularization trên dynamics norm: thêm vào loss.
Khoảng tích phân
Với dữ liệu trải dài nhiều tháng, đừng integrate từ đến phút:
t_normalized = (timestamps - timestamps[0]) / (timestamps[-1] - timestamps[0])
Điều này giữ solver trong regime thân thiện về số và làm learned dynamics invariant theo scale. Nó cũng quan trọng với comparison: ODE-RNN không normalize có thể thua GRU baseline chỉ vì lý do số học, đây sẽ là measurement artifact chứ không phải finding.
Xử lý nhiều thang thời gian
Thị trường có động lực học ở các scale microsecond, second, minute và daily cùng lúc, và một Neural ODE duy nhất có thể khó giữ tất cả. Các lựa chọn: xếp chồng nhiều ODE block ở các scale khác nhau; mở rộng hidden dimension cho cả capacity nhanh và chậm; hoặc chạy Neural ODE riêng cho từng frequency band rồi fuse output. (Đây là câu hỏi về model capacity, khác với câu hỏi về backtest fidelity trong adaptive resolution drill-down.)
Các hướng nghiên cứu mở

Neural Jump SDE: thêm một jump component được học cho các dislocation đột ngột (flash crash, earnings surprise):
trong đó là compensated Poisson random measure và là jump kernel được học.
Neural Controlled Differential Equations (Neural CDE): thay Wiener process bằng general driving signal, để data stream quan sát được điều khiển dynamics. Điều này tự nhiên với order flow, nơi stream trade và quote điều khiển latent market state.
Differentiable Market Simulation: dùng Neural SDE làm generative model bên trong differentiable simulator và training strategy end-to-end bằng cách backpropagate qua nó thông qua adjoint. Strategy và market model cùng tiến hóa.
Physics-Informed Neural ODE: PINN nhúng differential-equation residual vào loss — bản thân kỹ thuật được giới thiệu trong bài Navier-Stokes. Ứng dụng tài chính là áp đặt no-arbitrage, put-call parity và martingale condition như penalty term hoặc hard constraint lên dynamics đã học.
Kết luận

Framing continuous-time đúng về mặt cấu trúc: thị trường là các process liên tục được quan sát tại những thời điểm rời rạc, không đều, và model nên tôn trọng điều đó. Toolchain đã trưởng thành — torchdiffeq, torchsde, plain PyTorch cho phần còn lại — và các chi phí đã biết là tốc độ solver cùng numerical stability trên khoảng tích phân dài.
Điều chưa được xác lập là phần quyết định liệu bất kỳ thứ nào trong số này có thuộc production stack hay không. Structural correctness không phải evidence của predictive advantage, và lợi thế cụ thể được claim cho continuous dynamics so với feature chưa được đo trên trade data thực ở đây. Cho đến khi bảng H2 ở trên được điền, hãy coi mọi phần bên dưới là hypothesis được đặc tả rõ với một implementation hoạt động đi kèm, không phải result.
Tài liệu tham khảo
- Chen, R.T.Q., Rubanova, Y., Bettencourt, J., Duvenaud, D. (2018). Neural Ordinary Differential Equations. NeurIPS 2018. arXiv:1806.07366
- Rubanova, Y., Chen, R.T.Q., Duvenaud, D. (2019). Latent ODEs for Irregularly-Sampled Time Series. NeurIPS 2019. arXiv:1907.03907
- Jia, J., Benson, A.R. (2019). Neural Jump Stochastic Differential Equations. NeurIPS 2019.
- Kidger, P., Morrill, J., Foster, J., Lyons, T. (2020). Neural Controlled Differential Equations for Irregular Time Series. NeurIPS 2020.
- Hasan, A., Pereira, J.M., Farsiu, S., Carin, L. (2021). Neural Network Stochastic Differential Equation Models with Applications to Financial Data Forecasting. arXiv:2111.13164
- torchdiffeq: github.com/rtqichen/torchdiffeq
- UvA Deep Learning Tutorials — Neural ODEs: uvadlc-notebooks.readthedocs.io
Tác Giả
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.