Irregular Time in Tick Models: Continuous-Time Encodings vs. Plain Positional Embeddings
Time bar เป็นการบีบอัดตามอำเภอใจที่ทำลายจังหวะของ event และ activity ควรเป็นตัวกำหนดขอบเขต bar แทน — เหตุผลนี้อธิบายครบ พร้อม bar 17 ชนิดและ generator ที่ใช้งานได้ ใน ประเภท Bar และวิธี Aggregation สำหรับ Algorithmic Trading บทความนี้เริ่มต่อจากจุดนั้นหนึ่งขั้น ที่ส่วนที่ยังไม่มีใครแก้: แม้เปลี่ยนมาใช้ tick, volume หรือ imbalance bar แล้ว แต่ โมเดล ที่รับข้อมูลก็ยังสมมติระยะห่างสม่ำเสมอ nn.TransformerEncoder ที่มี learned positional embedding มอง position 7 เป็น "ช่องที่เจ็ด" ไม่ว่า tick นั้นจะมาถึงหลัง position 6 เป็นเวลา 200 microsecond หรือ 40 วินาที Inter-arrival time — สิ่งที่ activity bar ถูกสร้างมาเพื่อรักษา — ถูกทิ้งไปอีกครั้งที่ input layer
ดังนั้นคำถามของบทความนี้จึงแคบและทดสอบได้: ควรบอก sequence model อย่างไรว่า tick เกิดขึ้นเมื่อใดจริง และคำตอบที่ซับซ้อนกว่าชนะคำตอบธรรมดาหรือไม่?
Background ที่สมมติว่ามี

สิ่งต่อไปนี้เป็น prerequisite ซึ่งมีอยู่แล้วในบล็อกนี้และจะไม่ derive ใหม่:
- Microstructure noise และ bid-ask bounce. Roll's implicit spread model derive negative serial covariance ใน tick return จาก first principles ในหน่วยราคา พร้อมชี้ implementation error ที่พบบ่อย: Bid-Ask Spread Modeling ด้วย ML
- ฟีเจอร์ Order book. Book imbalance, weighted mid-price, depth ratio ระดับ 1-5, book pressure และ spread/tick ratio แสดงในบทความเดียวกัน; derivation ของ OBI และ volume-weighted-mid — รวม caveat ว่า weighted mid ไม่ใช่ microprice ของ Stoikov — อยู่ใน DeepLOB: Deep Learning บน Limit Order Books
- Order flow และ trade intensity. Trade imbalance, VPIN และ Kyle's lambda อยู่ในบทความ spread-modeling; inter-order interval และ Hawkes self-excitation intensity ในฐานะ timing feature อยู่ใน Digital Fingerprint: Trader Identification ค่า intensity แบบหยาบ
1/dtที่ draft เดิมเสนออ่อนแอกว่า Hawkes formulation ที่นั่น - Intraday non-stationarity. รูปแบบ volume ตัว U, spread ที่กว้างขึ้นในช่วงเงียบ และ time-of-day sin/cos encoding ครอบคลุมใน spread-modeling ในฐานะ market regime feature
- Label. convention ของ smoothing ทั้งสองแบบ (future-mean เทียบ current price, future-mean เทียบ previous-mean), three-class discretization ที่มี threshold และคำเตือนของ LOBFrame เรื่อง label sensitivity ต่อ และ อยู่ใน บทความ DeepLOB
- Class imbalance. เมื่อมี dead zone, FLAT class จะครอง ดังนั้น F1 ไม่ใช่ accuracy จึงเป็น metric ที่รายงานได้ — บทความเดียวกัน
- ระยะเวลาเติม Bar. Wall-clock time ที่ volume bar ใช้เติมเป็น feature ที่ใช้ได้สำหรับ tick model; generator ที่สร้างมันอยู่ใน ประเภท Bar และวิธี Aggregation
ควรระบุให้ชัดเพราะ draft รุ่นแรกเข้าใจผิด: การกระโดดจาก accuracy ทิศทาง 50.0% เป็น 50.5% ไม่ได้แปลว่าทำกำไรได้โดยอัตโนมัติ ประเด็นหลักของ LOBFrame ในบทความ DeepLOB คือ move ที่ทำนายต้องใหญ่พอจะข้าม spread ก่อนที่ accuracy จะมีความหมาย และ honest negative result ในบล็อกนี้แสดงสิ่งที่เกิดขึ้นเมื่อสมมติเป็นอย่างอื่น
Baseline

สมมติ DeepLOB-style CNN-Inception-LSTM เป็น baseline encoder; architecture, ตัวเลข FI-2010 Setup 2 ที่ตรวจสอบแล้ว (F1 83.40 / accuracy 84.47 ที่ ), caveat ของ LOBFrame และ PyTorch reimplementation ที่ทำงานได้อยู่ใน DeepLOB: Deep Learning บน Limit Order Books ไม่มีอะไรด้านล่างเปลี่ยน encoder นั้น — คำถามทั้งหมดคือสิ่งใดถูกเพิ่มเข้าไปใน input representation
สามวิธี Encode เวลาไม่สม่ำเสมอ

Option A: delta_t เป็น feature ธรรมดา
คำตอบที่ง่ายที่สุด ต่อท้าย (แปลง log และ z-score แล้ว) เป็นอีกคอลัมน์ใน feature vector ข้าง OFI และ book imbalance แล้วคง learned positional embedding มาตรฐานไว้ ไม่เสียอะไร เพิ่ม input dimension เดียว และเป็นสิ่งที่ production tick model ส่วนใหญ่ทำจริง
นี่คือ arm ที่ proposal แฟนซีทุกอันต้องชนะ และเป็น arm ที่มักถูกละไว้ใน paper ที่เสนอทางเลือกซับซ้อนกว่า
Option B: continuous-time positional encoding
แทน discrete positional embedding ด้วย sinusoidal function ของ elapsed time บน learnable timescale:
โดย เป็น learnable timescale parameter ที่ initialize แบบ log-spaced จาก microsecond ถึง second ผลที่ตั้งใจคือ attention สามารถให้น้ำหนัก event ตาม temporal relevance แทน slot index — trade ใหญ่เมื่อ 200 microsecond ที่แล้วควรเข้าถึงต่างจาก trade เล็กเมื่อ 50 millisecond ที่แล้ว
ส่วนที่ learnable น่าสนใจที่สุด หาก initialize timescale แบบ log-spaced จาก 1 microsecond ถึง 10 วินาทีแล้ว train, ตำแหน่งที่ timescale ไปจบลงคือ measurement เอง: ถ้ามัน collapse ไปทาง millisecond end โมเดลกำลังบอกว่าทุกอย่างหลังไม่กี่ millisecond แยกกันไม่ออก และนั่นเป็น finding เกี่ยวกับตลาด ไม่ใช่ architecture
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)
เมื่อใส่ลงใน standard encoder มันแทนที่เพียงหนึ่งบรรทัด — positional embedding add:
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, :])
Encoder boilerplate เองไม่ใช่ของใหม่ — scaffolding nn.TransformerEncoderLayer เดียวกันตีพิมพ์เป็น Architecture 2 ใน spread modeling สิ่งใหม่มีเพียง ContinuousTimeEncoding และ learnable-timescale argument
Option C: continuous latent state ระหว่าง tick (ODE-RNN)
Option ที่ principled ที่สุดถือ latent state เป็น continuous-time process ที่โมเดลด้วย Neural ODE (Chen et al., 2018) ระหว่าง tick hidden state วิวัฒน์ตาม differential equation ที่เรียนรู้:
เมื่อ tick มาถึง state จะ update ด้วย observation:
นี่คือ ODE-RNN framework มันรองรับ spacing ไม่สม่ำเสมอในเชิงโครงสร้าง แทนที่จะเป็น input feature: solver integrate เป็นเวลา พอดีระหว่าง event โดยไม่มี padding, interpolation หรือ resampling step ที่ทำให้ข้อมูลหาย
ต้นทุนคือ computational cost ODE solver ทำงาน sequential และ parallelize ยาก ทำให้ arm นี้มีโอกาสน้อยที่สุดที่จะผ่าน latency budget — ดูวินัยด้าน latency ใน The IPC Tax และ backtest engine speed ladder ว่าควรวัด claim อย่างไรก่อนกล่าวอ้าง มันถูกใส่ไว้เป็น upper bound ของสิ่งที่ explicit time modeling ซื้อได้ ไม่ใช่ deployment candidate
Event-Weighted Attention

อีกไอเดียที่ orthogonal: tick ไม่ได้ให้ข้อมูลเท่ากันทั้งหมด trade 100 หุ้นที่ bid เป็นเรื่องปกติ trade ที่กวาดสาม price level คือ regime change เรา inject prior นี้เข้า attention โดยตรงได้ กำหนด event importance score:
โดย คือ trade size, คือ price change, คือ volatility ล่าสุด และ flag multi-level sweep เพิ่มมันเข้า attention logits ก่อน softmax:
โดย โมเดลยังเรียนรู้ arbitrary attention pattern ได้ แต่เริ่มด้วย bias ไปทาง trade ที่ขยับตลาด
นี่เป็นสูตรที่คิดขึ้นเอง functional form, การเลือกสาม term และ softplus ล้วนเป็น guess และไม่มีอะไรที่นี่แสดงว่า bias ช่วยแทนที่จะเผา capacity
Multi-Horizon Heads

Horizon ต่างกันให้ข้อมูลแก่การตัดสินใจต่างกัน, encoder ร่วมที่มี output หลาย horizon ดีกว่า model แยกต่อ horizon และ joint loss ทำ regularization — เหตุผลนี้ รวม quantile output และ interpretability อยู่ใน Temporal Fusion Transformer สำหรับ Trading ส่วนที่เฉพาะกับ tick คือชุด horizon: 1, 10, 50 และ 100 event แทนวัน ซึ่งหมายความว่า horizon ทับซ้อนกันมากใน wall-clock time ระหว่าง burst และแทบไม่ทับกันในช่วงเงียบ — ความซับซ้อนที่ literature horizon รายวันไม่ต้องรับมือ
ต้องวัด Purging เป็น Event

Purged walk-forward validation อธิบายครบใน Walk-Forward Optimization (anchored, rolling, combinatorial purged CV, WFER, degradation rate) และ purged_walk_forward() ที่ใช้งานได้พร้อม purge และ embargo gap ชัดเจนอยู่ใน spread modeling look-ahead bias taxonomy วัดว่าการรั่วชนิดนี้ทำอะไรกับ Sharpe ที่รายงาน
สิ่งเดียวที่เฉพาะกับ tick data: tick ที่ติดกันห่างกันระดับ millisecond เป็น near-duplicate ดังนั้น purge gap ที่วัดเป็น วัน ไม่มีความหมายเมื่อ burst ใส่ sample เกือบเหมือนกัน 500 ตัวในหนึ่งวินาที Gap ต้องวัดเป็น event และขนาดที่ถูกต้องคือคำถามเชิงประจักษ์เกี่ยวกับ burst structure ของ instrument ไม่ใช่ค่าคงที่ที่ copy จาก paper
Claim นี้ทดสอบได้ราคาถูก — sweep purge gap เป็น event แล้ว plot validation F1 เทียบกับมัน หาก validation F1 ลดลงเมื่อ gap กว้างขึ้นแล้ว flatten จุด flatten คือ gap ของคุณ; หากไม่ลดเลย adjacent-tick leakage อาจไม่ใช่ปัญหาที่คิด
Experiment ที่ตัดสินเรื่องนี้

ทุกอย่างด้านบนคือ architecture ไม่มีสิ่งใดเป็น evidence บทความยังไม่ผ่านเกณฑ์ blog จนกว่าจะรันสิ่งต่อไปนี้:
Setup. Fix dataset เดียว (BTC/USDT trades คือ house dataset), encoder เดียว, label definition เดียว และ purged split เดียว เปลี่ยนเพียงหนึ่งสิ่งเท่านั้น
สาม arm.
| Arm | ข้อมูลเวลา | Positional encoding |
|---|---|---|
| A | เป็น raw input feature | plain learned positional embedding |
| B | ไม่มีนอกเหนือจาก encoding | ContinuousTimeEncoding (learnable timescale) |
| C | เป็น feature และ continuous-time encoding | ContinuousTimeEncoding |
สิ่งที่ต้อง report.
- F1 ต่อ class ไม่ใช่ accuracy — ภายใต้ dead-zone label FLAT class ครอง และ accuracy ไม่มีข้อมูลด้วยเหตุผลเดียวกับที่บทความ DeepLOB อธิบาย
- Confidence interval จาก run ซ้ำด้วย seed ต่างกัน F1 gap 0.4 จุดระหว่าง arm ไม่มีความหมายหากไม่มีหนึ่งอัน
- Timescale ที่ fit แล้ว พิมพ์
torch.exp(model.time_encoding.log_timescales)หลัง train ตำแหน่งที่มันไปจบคือเลขที่น่าสนใจที่สุดใน experiment และใช้เพียงหนึ่งบรรทัด - Hardware และ wall-clock cost ต่อ arm ในสไตล์ The IPC Tax — วัดบน hardware ที่เปิดเผย median จาก N หาก arm B ใช้ inference time 3x ของ arm A เพื่อ F1 เพียงเศษส่วน นั่นคือคำตอบ
หากมี negative result ให้ report ด้วย "Continuous-time encoding ไม่ได้ให้อะไรเหนือการป้อน เป็น feature บน BTC tick 30 วัน" มีประโยชน์กว่า survey ของ architecture สามแบบที่ไม่มีใครวัด และยังสอดคล้องกับ finding ทั่วไปในบล็อกนี้ว่า edge ที่สร้างอย่างระมัดระวังมักระเหยภายใต้ validation ที่ซื่อสัตย์
สถานะปัจจุบัน

คำถามที่ยังเปิดอยู่เป็นเรื่องจริง: sequence model ที่รับ event ระยะห่างไม่สม่ำเสมอยังคง encode position แทน time และ coverage เดิมของบล็อก — รวม Transformer encoder ใน spread modeling — ใช้ plain learned positional embedding Continuous-time encoding, ODE-RNN latent state และ event-weighted attention เป็นสามวิธีแก้ปัญหานั้น
สิ่งที่ขาดคือ evidence ว่าการแก้มีความสำคัญ ทั้งสามตอนนี้เป็นหัวข้อ ไม่ใช่ finding การวัดที่ตัดสินคือ three-arm ablation บน encoder คงที่และ purged split คงที่ รายงาน per-class F1 พร้อม confidence interval และ fitted timescale — และยังไม่ได้รัน
ผู้เขียน
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.