📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← 返回文章列表
August 22, 2026
5 分鐘閱讀

逐筆資料模型中的不規則時間:連續時間編碼與純位置嵌入

逐筆資料模型中的不規則時間:連續時間編碼與純位置嵌入
#HFT
#tick-data
#deep-learning
#high-frequency
#prediction

時間棒是一種會破壞事件時間資訊的任意壓縮;應該由活動定義棒的邊界——演算法交易的棒類型與聚合方法 一文完整論述了這一點,包含 17 種棒類型與可運作的產生器。本文從下一步開始,處理尚未有人解決的部分:即使你已經改用逐筆、成交量或不平衡棒,餵給它們的 模型 仍假設間隔規則。帶有學習式位置嵌入的 nn.TransformerEncoder 會把位置 7 視為「第七個槽位」,不論該筆成交是在位置 6 後 200 微秒還是 40 秒才抵達。事件間隔時間——活動棒原本要保留的資訊——又在輸入層被丟掉了。

因此,本文的問題很狹窄也可檢驗:應該如何告訴序列模型某筆成交究竟何時發生?而精巧的答案是否勝過簡單的答案?

預備背景

不規則事件脈衝

以下都是前置知識,本部落格已經逐一介紹,本文不會重新推導:

  • 微結構雜訊與買賣價差反彈。 Roll 的隱含價差模型從第一原理、以價格單位推導逐筆報酬的負序列共變異,並指出常見的實作錯誤:使用機器學習建模買賣價差
  • 訂單簿特徵。 訂單簿不平衡、加權中間價、第一至第五檔的深度比、訂單簿壓力,以及價差/跳動比率都列在同一篇文章中;OBI 與成交量加權中間價的推導——包括加權中間價 不是 Stoikov microprice 的注意事項——見 DeepLOB:限價訂單簿上的深度學習
  • 訂單流與成交強度。 成交不平衡、VPIN 與 Kyle lambda 見價差建模文章;訂單間隔與 Hawkes 自激強度作為時間特徵,見 數位指紋:交易者識別。本草稿原先提出的粗略 1/dt 強度,是其中 Hawkes 公式的較弱版本。
  • 日內非平穩性。 U 形的成交量模式、安靜時段擴大的價差,以及時段 sin/cos 編碼,都在價差建模文章中作為市場狀態特徵介紹。
  • 標籤。 兩種平滑慣例(未來平均值對當前價格、未來平均值對前一平均值)、帶閾值 α\alpha 的三類離散化,以及 LOBFrame 對標籤受 α\alphakk 敏感的警告,都在 DeepLOB 文章 中。
  • 類別不平衡。 有了死區後 FLAT 類別會占主導,因此可報告的指標是 F1 而非準確率——同一篇文章已說明。
  • 棒填充時間。 一根成交量棒填滿所需的實際時鐘時間,可作為逐筆模型的特徵;產生它的生成器見 棒類型與聚合方法

有一點必須明確說明,因為本草稿的原始版本在這裡弄錯了:方向準確率從 50.0% 升到 50.5%,並不能不證自明地帶來獲利。LOBFrame 的核心觀點(DeepLOB 文章有討論)是,預測的價格變動必須大到足以先跨過價差,準確率才有意義;本部落格的正直負結果正是你做出相反假設時會發生的事。

基準模型

基準序列事件編碼

假設以 DeepLOB 為風格的 CNN-Inception-LSTM 作為基準編碼器;架構、已驗證的 FI-2010 Setup 2 數字(k=10k=10 時 F1 83.40/準確率 84.47)、LOBFrame 的注意事項,以及可運作的 PyTorch 重新實作,都在 DeepLOB:限價訂單簿上的深度學習。以下不改變該編碼器,唯一問題是要在其輸入表徵中加入什麼。

編碼不規則時間的三種方法

不規則時間的替代編碼

選項 A:將 delta_t 作為一般特徵

最簡單的答案。把 Δti=titi1\Delta t_i = t_i - t_{i-1}(經過對數轉換、z-score 標準化)作為特徵向量的另一欄,與 OFI 和訂單簿不平衡並列,保留標準的學習式位置嵌入。成本為零,只增加一個輸入維度,實際上大多數生產環境的逐筆模型就是這樣做。

這是所有更花俏提案都必須擊敗的基準,也是提出更花俏替代方案的論文常常省略的基準。

選項 B:連續時間位置編碼

以經過時間的正弦函數取代離散位置嵌入,並使用可學習的時間尺度:

TE(Δt)=[sin(Δtτ1),cos(Δtτ1),,sin(Δtτd),cos(Δtτd)]\text{TE}(\Delta t) = \left[\sin\left(\frac{\Delta t}{\tau_1}\right), \cos\left(\frac{\Delta t}{\tau_1}\right), \ldots, \sin\left(\frac{\Delta t}{\tau_d}\right), \cos\left(\frac{\Delta t}{\tau_d}\right)\right]

其中 τ1,,τd\tau_1, \ldots, \tau_d 是可學習的時間尺度參數,初始化時以對數間隔涵蓋微秒到秒。預期效果是注意力能依時間相關性而非槽位索引對事件加權——200 微秒前的一筆大額成交,應能以不同於 50 毫秒前小額成交的方式被取用。

可學習的部分才是有趣之處。如果把時間尺度從 1 微秒到 10 秒以對數間隔初始化並進行訓練,時間尺度最後落在哪裡本身就是一項測量:如果它們都塌縮到毫秒端,模型便是在告訴你幾毫秒以外的一切都可互換;這是關於市場的發現,而不是關於架構的發現。

import numpy as np
import torch
import torch.nn as nn

class ContinuousTimeEncoding(nn.Module):
    """Sinusoidal encoding for irregular inter-arrival times."""
    def __init__(self, d_model: int, num_timescales: int = 64):
        super().__init__()
        log_timescales = torch.linspace(
            np.log(1e-6), np.log(10.0), num_timescales
        )
        self.log_timescales = nn.Parameter(log_timescales)
        self.proj = nn.Linear(num_timescales * 2, d_model)

    def forward(self, delta_t: torch.Tensor) -> torch.Tensor:
        """
        Args:
            delta_t: (batch, seq_len) inter-arrival times in seconds
        Returns:
            (batch, seq_len, d_model) time encoding
        """
        timescales = torch.exp(self.log_timescales)  # (num_timescales,)
        scaled = delta_t.unsqueeze(-1) / timescales.unsqueeze(0).unsqueeze(0)
        encoding = torch.cat([torch.sin(scaled), torch.cos(scaled)], dim=-1)
        return self.proj(encoding)

放入標準編碼器後,它只取代一行——加入位置嵌入的那一行:

class TickTransformer(nn.Module):
    def __init__(self, input_dim: int, d_model: int = 128,
                 nhead: int = 8, num_layers: int = 4, num_classes: int = 3):
        super().__init__()
        self.feature_proj = nn.Linear(input_dim, d_model)
        self.time_encoding = ContinuousTimeEncoding(d_model)
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=nhead,
            dim_feedforward=d_model * 4,
            dropout=0.1, batch_first=True
        )
        self.encoder = nn.TransformerEncoder(
            encoder_layer, num_layers=num_layers
        )
        self.head = nn.Linear(d_model, num_classes)

    def forward(self, features: torch.Tensor,
                delta_t: torch.Tensor) -> torch.Tensor:
        h = self.feature_proj(features) + self.time_encoding(delta_t)
        h = self.encoder(h)
        return self.head(h[:, -1, :])

編碼器的樣板程式本身並不新——相同的 nn.TransformerEncoderLayer 骨架已經在價差建模的架構 2 中發布。新增的只有 ContinuousTimeEncoding 與可學習時間尺度這個論點。

選項 C:逐筆成交之間的連續潛在狀態(ODE-RNN)

最有原則性的選項,把潛在狀態視為由 Neural ODE 建模的連續時間過程(Chen et al., 2018)。在兩筆成交之間,隱藏狀態依照學習到的微分方程演化:

dhdt=fθ(h(t),t)\frac{d\mathbf{h}}{dt} = f_\theta(\mathbf{h}(t), t)

當一筆成交抵達時,利用觀測值更新狀態:

h(ti+)=Update(h(ti),xi)\mathbf{h}(t_i^+) = \text{Update}(\mathbf{h}(t_i^-), \mathbf{x}_i)

這就是 ODE-RNN 框架。它以結構方式處理不規則間隔,而不是把間隔當作輸入特徵:求解器會在事件之間精確積分 Δti\Delta t_i,不需要填充、插值或重新取樣,因此不會在那些步驟中遺失資訊。

代價是計算量。ODE 求解器具有序列性且難以平行化,因此最不可能通過延遲預算的是這個選項——關於在提出此類主張前應如何測量,請參閱 IPC 稅回測引擎速度階梯 所建立的延遲紀律。這裡把它列為顯式時間建模能帶來的上限,而不是部署候選方案。

事件加權注意力

事件加權注意力場

另一個正交想法:並非所有逐筆成交都同樣有資訊量。買方一筆 100 股的成交很平常;掃過三個價格檔位的成交則代表狀態改變。我們可以直接把這個先驗注入注意力。定義事件重要性分數:

wi=softplus(α1log(qi)+α2Δpi/σ+α31[sweepi])w_i = \text{softplus}\left(\alpha_1 \cdot \log(q_i) + \alpha_2 \cdot |\Delta p_i| / \sigma + \alpha_3 \cdot \mathbb{1}[\text{sweep}_i]\right)

其中 qiq_i 是成交量,Δpi\Delta p_i 是價格變化,σ\sigma 是近期波動率,而 sweepi\text{sweep}_i 則標記跨越多個檔位的掃單。在 softmax 前把它加到注意力 logits:

Attention(Q,K,V)=softmax(QKTdk+W)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + \mathbf{W}\right) V

其中 Wij=wj\mathbf{W}_{ij} = w_j。模型仍保留學習任意注意力模式的能力,但一開始會偏向那些推動市場的成交。

這是一個臨時捏造的公式。函數形式、三個項的選擇以及 softplus 全都是猜測,本文沒有任何證據表明這種偏置有幫助,而不是只是在消耗模型容量。

多重時間範圍的輸出頭

多重時間範圍預測帶

不同時間範圍會支援不同決策;共享編碼器搭配多個時間範圍輸出勝過每個時間範圍各自訓練的模型,而聯合損失具有正則化作用——這套論證,以及分位數輸出與可解釋性,見 用於交易的 Temporal Fusion Transformer。唯一逐筆資料特有的部分是時間範圍集合:不是天數,而是 1、10、50、100 個 事件;這代表突發期間它們在實際時鐘時間上高度重疊,而安靜期間幾乎完全不重疊——這是日線時間範圍文獻不必處理的複雜性。

清洗必須以事件衡量

事件空間排除區間

清洗式 walk-forward 驗證的完整流程見 Walk-Forward Optimization(錨定、滾動、組合式清洗 CV、WFER、退化率),而帶有明確清洗與禁運區間的可運作 purged_walk_forward() 位於價差建模前視偏差分類量化這類洩漏對報告夏普值的影響。

逐筆資料特有的一點是:相隔幾毫秒的相鄰成交幾乎是重複樣本,因此以 為單位的清洗間隔,在突發期間一秒內塞入 500 個近乎相同樣本時毫無意義。間隔必須以 事件 為單位設定,而正確大小是關於你的工具突發結構的經驗問題,不是可以從論文複製的常數。

這項主張很容易測試——掃描以事件為單位的清洗間隔,並繪製驗證 F1 對間隔的關係。如果驗證 F1 隨間隔擴大而下降,之後趨於平坦,那個趨平點就是你的間隔;如果它從不下降,相鄰成交洩漏就不是你以為的問題。

決定性實驗

受控的時間模型評估

以上全部都是架構,沒有任何一項是證據。在完成以下執行之前,本文還沒有達到本部落格的標準:

設定。 固定一份資料集(BTC/USDT 成交是本項目的標準資料集)、一個編碼器、一個標籤定義與一個清洗切分,只改變一件事。

三個分支。

分支 時間資訊 位置編碼
A Δt\Delta t 作為原始輸入特徵 標準學習式位置嵌入
B 除編碼以外沒有其他資訊 ContinuousTimeEncoding(可學習時間尺度)
C Δt\Delta t 作為特徵 並且 使用連續時間編碼 ContinuousTimeEncoding

要報告的內容。

  1. 每個類別的 F1,不是準確率——在帶死區的標籤下 FLAT 類別占主導,而準確率正如 DeepLOB 文章所說,沒有資訊量。
  2. 信賴區間,來自不同隨機種子的重複執行。沒有信賴區間,分支間 0.4 個 F1 點的差距毫無意義。
  3. 擬合出的時間尺度。 訓練後列印 torch.exp(model.time_encoding.log_timescales)。它們最後穩定的位置是實驗中最有趣的數字,而且只需一行就能取得。
  4. 各分支的硬體與實際時鐘成本,格式參照 IPC 稅——在公開硬體上測量,取 N 次的中位數。如果分支 B 的推理時間是分支 A 的 3 倍,卻只換來不到一個 F1 點,答案就很清楚了。

如果有負結果,請報告它。「在 30 天 BTC 逐筆資料上,連續時間編碼並沒有比把 Δt\Delta t 當作特徵更好」會比一篇沒有人測量的三種架構綜述更有用。這也符合本部落格一貫的發現:經過精心構造的優勢,往往會在誠實驗證下蒸發。

現況

連續時間研究路徑

尚未解答的問題是真實存在的:餵入不規則間隔事件的序列模型仍然編碼位置而非時間,而本部落格現有的內容——包括價差建模中的 Transformer 編碼器——使用的是普通的學習式位置嵌入。連續時間編碼、ODE-RNN 潛在狀態與事件加權注意力,是修正這一點的三種方法。

缺少的是修正它確實有用的證據。這三者目前都是題目,而非發現。決定性的測量是在固定編碼器與固定清洗切分上進行三分支消融,報告帶信賴區間的各類別 F1 與擬合出的時間尺度——而這項實驗尚未執行。

免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

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 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。