Irregular Time in Tick Models: Continuous-Time Encodings vs. Plain Positional Embeddings
时间 bar 是一种任意的压缩,会破坏事件时序;相反,应由活动来定义 bar 的边界——算法交易的 Bar 类型与聚合方法中完整论证了这一点,并提供了 17 种 bar 类型和可运行的生成器。本文从下一步开始,讨论尚未解决的部分:即使已经切换到 tick、成交量或不平衡 bar,输入这些数据的模型仍然假设间隔规则均匀。带有学习位置嵌入的 nn.TransformerEncoder 会把位置 7 当作“第七个槽位”,无论该 tick 是在位置 6 后 200 微秒还是 40 秒到达。事件到达间隔时间——活动 bar 构造时本来要保留的东西——又在输入层被丢掉了。
因此,本文的问题很狭窄,也可以检验:应该如何告诉序列模型一个 tick 实际发生的时间,以及复杂方案是否真的胜过简单方案?
预备背景

以下内容都是前置知识,已在本博客中覆盖,本文不再重新推导:
- 微观结构噪声与买卖价反弹。 Roll 的隐含价差模型从第一原理出发,在价格单位中推导 tick 收益的负序列协方差,并指出常见的实现错误:见用机器学习建模买卖价差。
- 订单簿特征。 同一篇文章列出了订单簿不平衡、加权中间价、1–5 档深度比、订单簿压力和价差/tick 比率;OBI 与成交量加权中间价的推导(包括加权中间价不是 Stoikov microprice 的提醒)见DeepLOB:限价订单簿上的深度学习。
- 订单流与交易强度。 交易不平衡、VPIN 和 Kyle lambda 见价差建模文章;订单间隔和 Hawkes 自激强度作为时间特征见数字指纹:交易者识别。本文初稿提出的粗糙
1/dt强度是那种 Hawkes 表述的较弱版本。 - 日内非平稳性。 U 形成交量模式、安静时段扩大的价差,以及一天中的时间 sin/cos 编码,均在价差建模文章的市场状态特征中介绍。
- 标签。 两种平滑约定(未来均值与当前价格、未来均值与前一均值)、带阈值 的三分类离散化,以及 LOBFrame 关于标签对 和 敏感的警告,都在 DeepLOB 文章中。
- 类别不平衡。 存在死区时 FLAT 类占主导,因此 F1 而非准确率才是应报告的指标——同一篇文章已有说明。
- Bar 填充时长。 一个成交量 bar 填满所需的墙上时钟时间,可以作为 tick 模型的有效特征;生成它的生成器见Bar 类型与聚合方法。
有一点必须明确说明,因为本文初稿在此犯了错误:方向准确率从 50.0% 提升到 50.5%,并不能自然而然地证明有利可图。LOBFrame 的核心观点(DeepLOB 文章中有讨论)是,预测的变动必须大到足以越过价差,准确率才有意义;本博客的诚实负结果说明了假定相反结论会发生什么。
基线

假设以 DeepLOB 风格的 CNN-Inception-LSTM 作为基线编码器。其架构、已验证的 FI-2010 Setup 2 数字( 时 F1 83.40 / 准确率 84.47)、LOBFrame 的注意事项,以及可运行的 PyTorch 重实现,都在DeepLOB:限价订单簿上的深度学习中。下文不改变这个编码器,唯一的问题是向其输入表示中添加什么。
编码不规则时间的三种方式

方案 A:将 delta_t 作为普通特征
最简单的答案。将 (取对数变换并做 z-score)作为特征向量中的另一列,与 OFI 和订单簿不平衡并列,然后保留标准的学习位置嵌入。它不产生额外成本,只增加一个输入维度,也是多数生产 tick 模型实际采用的方案。
这是所有更复杂提案必须击败的基线,也是提出更复杂替代方案的论文常常省略的基线。
方案 B:连续时间位置编码
用经过时间的正弦函数替换离散位置嵌入,并使用可学习的时间尺度:
其中 是可学习的时间尺度参数,初始化时按对数间隔覆盖微秒到秒。预期效果是,注意力可以根据时间相关性而不是槽位索引为事件赋予权重——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:tick 之间的连续潜在状态(ODE-RNN)
最有原则的方案是把潜在状态视为由神经 ODE(Chen 等人,2018)建模的连续时间过程。在两个 tick 之间,隐藏状态按照学习得到的微分方程演化:
当一个 tick 到达时,使用观测值更新状态:
这就是 ODE-RNN 框架。它不是把不规则间隔当作输入特征,而是在结构上处理它:求解器严格按事件之间的 进行积分,不需要填充、插值或重采样,因此没有信息在这些步骤中丢失。
代价是计算量。ODE 求解器是串行的,难以并行化,这使它最不可能通过延迟预算——关于这类主张应如何在提出前测量,请参见IPC 税和回测引擎速度阶梯中确立的延迟纪律。本文将其作为显式时间建模能够带来的上限,而不是部署候选方案。
事件加权注意力

另一个正交思路是:并非所有 tick 都同样有信息量。买一档的 100 股交易很普通,扫过三个价位的交易则意味着状态变化。我们可以直接把这个先验注入注意力。定义事件重要性分数:
其中 是交易量, 是价格变化, 是近期波动率, 标记多档扫单。在 softmax 之前把它加到注意力 logits 上:
其中 。模型仍能学习任意注意力模式,但初始时会偏向于推动市场的交易。
这是一个人为发明的公式。函数形式、三个项的选择以及 softplus 都只是猜测,本文没有显示这种偏置有帮助,而不是仅仅消耗模型容量。
多时域输出头

不同的时域会影响不同决策,共享编码器加多时域输出优于每个时域单独训练模型,而且联合损失具有正则化作用——用于交易的时间融合 Transformer详细论证了这一点,并讨论了分位数输出和可解释性。唯一与 tick 特有的部分是时域集合:用 1、10、50 和 100 个事件,而不是天数。这意味着在突发期间,各时域在墙上时钟时间上高度重叠,而在安静期间几乎完全不重叠——这是日时域文献无需处理的复杂性。
清洗必须按事件衡量

有关净化滚动验证的完整流程见滚动前向优化(锚定、滚动、组合净化 CV、WFER、退化率),而带有明确净化和封锁间隔、可运行的 purged_walk_forward() 已随价差建模发布。前视偏差分类量化了这类泄漏对报告夏普比率的影响。
tick 数据特有的一点是:相隔几毫秒的相邻 tick 几乎是重复样本,因此按天设置的净化间隔,在一次突发于一秒内产生 500 个近乎相同样本时毫无意义。间隔必须按事件设置,而正确大小取决于工具的突发结构,是一个经验问题,不是可以从论文复制的常数。
这个主张很容易检验——扫描以事件计的净化间隔,并绘制验证 F1。若验证 F1 随间隔扩大而下降,随后趋于平坦,那么平坦点就是你的间隔;若它从不下降,那么相邻 tick 泄漏并不是你以为的问题。
决定这一点的实验

上文全部是架构,没有任何一项是证据。本文只有在完成以下运行后,才达到本博客的标准:
设置。 固定一个数据集(BTC/USDT 交易是本团队的数据集)、一个编码器、一种标签定义和一个净化划分,只改变一件事。
三组方案。
| 方案 | 时间信息 | 位置编码 |
|---|---|---|
| A | 作为原始输入特征 | 普通学习位置嵌入 |
| B | 除编码外没有其他时间信息 | ContinuousTimeEncoding(可学习时间尺度) |
| C | 作为特征,且使用连续时间编码 | ContinuousTimeEncoding |
需要报告的内容。
- 每个类别的 F1,而不是准确率——在死区标签下 FLAT 类占主导,准确率之所以没有信息,正是 DeepLOB 文章所解释的原因。
- 置信区间,来自不同随机种子的重复运行。没有置信区间,方案之间 0.4 个百分点的 F1 差距没有意义。
- 拟合出的时间尺度。 训练后打印
torch.exp(model.time_encoding.log_timescales)。它们最终落在哪里,是实验中最有趣的数字,而且只需一行代码即可得到。 - 每个方案的硬件和墙上时钟成本,按照IPC 税的风格测量:在公开硬件上报告 N 次运行的中位数。如果方案 B 的推理时间是方案 A 的 3 倍,却只换来 F1 的零点几分,这就是答案。
如果有负结果,就报告它。“在 30 天的 BTC tick 上,连续时间编码并不比将 作为特征输入更好”比一篇没有人测量过的三种架构综述更有用。这也与本博客的一般发现一致:精心构造的优势往往会在诚实验证下蒸发。
当前状态

这个未解决的问题确实存在:输入不规则间隔事件的序列模型仍然编码位置而不是时间,而本博客现有内容(包括价差建模中的 Transformer 编码器)使用普通的学习位置嵌入。连续时间编码、ODE-RNN 潜在状态和事件加权注意力,是解决这一问题的三种方式。
缺少的是修正它是否真的重要的证据。三者目前都是主题,而不是发现。决定性测量是在固定编码器和固定净化划分上进行三组消融,报告带置信区间的逐类 F1 和拟合时间尺度——这项测量尚未运行。
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.