Irregular Time in Tick Models: Continuous-Time Encodings vs. Plain Positional Embeddings
Le barre temporali sono una compressione arbitraria che distrugge la tempistica degli eventi; dovrebbero invece essere le attività a definire i confini delle barre. L'argomento è sviluppato per intero, con 17 tipi di barre e generatori funzionanti, in Tipi di barre e metodi di aggregazione per il trading algoritmico. Questo articolo parte dal problema successivo, ancora irrisolto: anche dopo il passaggio a barre di tick, volume o sbilanciamento, il modello a cui le si fornisce assume una spaziatura regolare. Un nn.TransformerEncoder con embedding posizionale appreso tratta la posizione 7 come "il settimo slot", sia che quel tick arrivi 200 microsecondi sia 40 secondi dopo la posizione 6. Il tempo tra gli arrivi — proprio ciò che le barre basate sull'attività cercano di conservare — viene nuovamente scartato a livello di input.
La domanda di questo articolo è quindi circoscritta e verificabile: come si comunica a un modello di sequenza quando si è verificato davvero un tick, e la risposta sofisticata è migliore di quella banale?
Prerequisiti

I seguenti sono prerequisiti, già trattati in questo blog; qui non vengono rielaborati:
- Rumore di microstruttura e bid-ask bounce. Il modello di spread implicito di Roll ricava dai primi principi la covarianza seriale negativa dei rendimenti tick, espressa in unità di prezzo; l'errore comune di implementazione è richiamato in Bid-Ask Spread Modeling with ML.
- Feature del book ordini. Squilibrio del book, mid-price ponderato, rapporto di profondità ai livelli 1-5, pressione del book e rapporto spread/tick sono tabulati nello stesso articolo. Le derivazioni di OBI e mid-price ponderato per il volume — inclusa la precisazione che il mid ponderato non è il microprice di Stoikov — sono in DeepLOB: Deep Learning on Limit Order Books.
- Flusso ordini e intensità degli scambi. Squilibrio degli scambi, VPIN e lambda di Kyle sono nell'articolo sulla modellazione dello spread; l'intervallo tra ordini e l'intensità di autoeccitazione di Hawkes come feature temporali sono in Digital Fingerprint: Trader Identification. La semplice intensità
1/dtproposta inizialmente in questa bozza è una versione più debole della formulazione di Hawkes. - Non stazionarietà intraday. Il profilo del volume a U, l'ampliamento dello spread nei periodi tranquilli e la codifica sin/cos dell'ora del giorno sono trattati nella modellazione dello spread come feature del regime di mercato.
- Etichette. Entrambe le convenzioni di smoothing (media futura rispetto al prezzo corrente, media futura rispetto alla media precedente), la discretizzazione in tre classi con soglia e l'avvertimento di LOBFrame sulla sensibilità dell'etichetta a e sono nell'articolo DeepLOB.
- Sbilanciamento delle classi. Con una dead zone domina la classe FLAT; per questo la metrica da riportare è F1, non l'accuratezza: si veda lo stesso articolo.
- Durata di riempimento della barra. Il tempo impiegato da una barra di volume per riempirsi è una feature utilizzabile da un modello tick; i generatori che la producono sono in Tipi di barre e metodi di aggregazione.
Vale la pena dirlo esplicitamente, perché la versione originale di questa bozza era errata: un aumento dell'accuratezza direzionale dal 50,0% al 50,5% non è evidentemente redditizio. Il punto centrale di LOBFrame, discusso nell'articolo DeepLOB, è che il movimento previsto deve essere abbastanza ampio da coprire lo spread prima che l'accuratezza abbia un significato; il risultato negativo onesto di questo blog mostra cosa succede quando si assume il contrario.
Baseline

Assumiamo come encoder di riferimento un CNN-Inception-LSTM in stile DeepLOB. Architettura, numeri verificati sul FI-2010 Setup 2 (F1 83,40 / accuratezza 84,47 a ), avvertenze su LOBFrame e una reimplementazione funzionante in PyTorch sono tutti in DeepLOB: Deep Learning on Limit Order Books. Nulla di ciò che segue modifica l'encoder: la domanda è soltanto cosa aggiungere alla sua rappresentazione di input.
Tre modi per codificare il tempo irregolare

Opzione A: delta_t come feature semplice
La risposta banale. Aggiungere (trasformato in log e standardizzato con z-score) come un'ulteriore colonna del vettore delle feature, accanto a OFI e squilibrio del book, mantenendo il normale embedding posizionale appreso. Non costa nulla, aggiunge una dimensione all'input ed è ciò che fanno davvero la maggior parte dei modelli tick in produzione.
Questa è la baseline che ogni proposta più sofisticata deve superare, ed è proprio quella che i lavori sulle alternative più elaborate spesso omettono.
Opzione B: codifica posizionale a tempo continuo
Sostituire l'embedding posizionale discreto con una funzione sinusoidale del tempo trascorso, su scale temporali apprendibili:
Qui sono parametri di scala temporale apprendibili, inizializzati con spaziatura logaritmica da microsecondi a secondi. L'obiettivo è che l'attenzione pesi gli eventi in base alla rilevanza temporale, non all'indice dello slot: una grande operazione avvenuta 200 microsecondi fa dovrebbe essere accessibile in modo diverso da una piccola operazione avvenuta 50 millisecondi fa.
La parte apprendibile è quella interessante. Se si inizializzano le scale temporali con spaziatura logaritmica da 1 microsecondo a 10 secondi e si addestra il modello, il punto in cui si stabilizzano le scale temporali è già una misura: se collassano verso l'estremo dei millisecondi, il modello sta dicendo che oltre pochi millisecondi tutto è intercambiabile. È un risultato sul mercato, non sull'architettura.
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)
Inserito in un encoder standard, sostituisce esattamente una riga, quella che aggiunge l'embedding posizionale:
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, :])
Il boilerplate dell'encoder non è una novità: lo stesso scaffolding nn.TransformerEncoderLayer è già pubblicato come Architettura 2 in spread modeling. Sono nuovi soltanto ContinuousTimeEncoding e l'argomento delle scale temporali apprendibili.
Opzione C: stato latente continuo tra i tick (ODE-RNN)
L'opzione più rigorosa tratta lo stato latente come un processo a tempo continuo modellato da una Neural ODE (Chen et al., 2018). Tra un tick e l'altro, lo stato nascosto evolve secondo un'equazione differenziale appresa:
Quando arriva un tick, lo stato viene aggiornato con l'osservazione:
Questo è il framework ODE-RNN. Gestisce la spaziatura irregolare nella struttura del modello, non come feature di input: il solver integra esattamente tra gli eventi, senza padding, interpolazione o ricampionamento che possa far perdere informazione.
Il costo è computazionale. I solver ODE sono sequenziali e difficili da parallelizzare, quindi questa soluzione è la meno probabile da far rientrare in un budget di latenza. Si vedano The IPC Tax e the backtest engine speed ladder per la disciplina con cui misurare simili affermazioni prima di farle. Qui serve come limite superiore di ciò che può offrire una modellazione temporale esplicita, non come candidata al deployment.
Attenzione pesata sugli eventi

Una seconda idea, ortogonale alla codifica temporale, è che non tutti i tick siano ugualmente informativi. Uno scambio di 100 azioni sul bid è routine. Un'operazione che attraversa tre livelli di prezzo segnala un cambio di regime. Possiamo inserire direttamente questo prior nell'attenzione. Definiamo un punteggio di importanza dell'evento:
Qui è la dimensione dello scambio, la variazione di prezzo, la volatilità recente e segnala un attraversamento di più livelli. Aggiungiamolo ai logit dell'attenzione prima della softmax:
con . Il modello conserva la capacità di apprendere pattern di attenzione arbitrari, ma parte con un bias verso gli scambi che hanno mosso il mercato.
Questa è una formula inventata. La forma funzionale, la scelta dei tre termini e l'uso di softplus sono ipotesi; qui non c'è alcuna evidenza che il bias aiuti invece di consumare semplicemente capacità del modello.
Teste multi-orizzonte

Orizzonti diversi informano decisioni diverse; un encoder condiviso con output su più orizzonti supera i modelli separati per orizzonte, mentre la loss congiunta regolarizza. L'argomento, insieme agli output quantili e all'interpretabilità, è sviluppato in Temporal Fusion Transformer for Trading. L'unico elemento specifico dei tick è l'insieme degli orizzonti: 1, 10, 50 e 100 eventi, non giorni. Durante i burst questi orizzonti si sovrappongono fortemente nel tempo reale, mentre nei periodi tranquilli quasi per nulla: una complicazione che la letteratura sugli orizzonti giornalieri non deve affrontare.
Il purging deve essere misurato in eventi

La validazione walk-forward con purging è trattata end-to-end in Ottimizzazione Walk-Forward (CV purgata ancorata, rolling e combinatoria, WFER, tasso di degradazione), mentre una purged_walk_forward() funzionante con purge ed embargo espliciti è disponibile in spread modeling. La tassonomia del look-ahead bias quantifica l'effetto di fughe di questo tipo sullo Sharpe dichiarato.
L'elemento specifico dei dati tick è questo: tick adiacenti separati da millisecondi sono quasi duplicati. Un gap di purge misurato in giorni non ha senso quando un burst inserisce 500 campioni quasi identici in un secondo. Il gap deve essere dimensionato in eventi; la dimensione corretta è una questione empirica della struttura dei burst dello strumento, non una costante da copiare da un articolo.
Questa affermazione è economica da testare: variare il purge gap in eventi e tracciare l'F1 di validazione in funzione del gap. Se l'F1 scende all'aumentare del gap e poi si stabilizza, il punto di stabilizzazione è il gap da usare; se non scende mai, la leakage dei tick adiacenti non era il problema ipotizzato.
L'esperimento decisivo

Tutto quanto sopra riguarda l'architettura. Nulla di tutto ciò costituisce una prova. L'articolo non supera lo standard di questo blog finché non viene eseguito quanto segue:
Configurazione. Fissare un dataset (le transazioni BTC/USDT sono il dataset interno), un encoder, una definizione delle etichette e uno split con purging. Variare esattamente una cosa.
Tre varianti.
| Braccio | Informazioni sull'orario | Codifica posizionale |
|---|---|---|
| A | come funzionalità di input non elaborata | incorporamento posizionale appreso semplice |
| B | nessuno, oltre la codifica | ContinuousTimeEncoding (tempi apprendibili) |
| C | come caratteristica e codifica a tempo continuo | ContinuousTimeEncoding |
Cosa riportare.
- F1 per classe, non accuratezza: con un'etichetta a dead zone domina la classe FLAT e l'accuratezza è non informativa esattamente per il motivo spiegato nell'articolo DeepLOB.
- Un intervallo di confidenza, ottenuto da esecuzioni ripetute con seed diversi. Una differenza di 0,4 punti F1 tra le varianti non significa nulla senza di esso.
- Le scale temporali apprese. Stampare
torch.exp(model.time_encoding.log_timescales)dopo l'addestramento. Il punto in cui si stabilizzano è il numero più interessante dell'esperimento e richiede una sola riga per essere ottenuto. - Costo hardware e tempo reale per variante, nello stile di The IPC Tax: misurato sull'hardware dichiarato, con mediana su N esecuzioni. Se la variante B costa 3 volte il tempo di inferenza della variante A per una frazione di punto F1, questa è la risposta.
Riportare il risultato negativo, se presente. "La codifica a tempo continuo non offre alcun vantaggio rispetto a fornire come feature, su 30 giorni di tick BTC" è un post più utile di una rassegna di tre architetture che nessuno ha misurato. Sarebbe inoltre coerente con il risultato generale di questo blog: i segnali costruiti con cura tendono a evaporare sotto una validazione onesta.
Stato attuale

La domanda lasciata scoperta è reale: i modelli di sequenza alimentati con eventi irregolarmente distanziati codificano ancora la posizione invece del tempo, e la trattazione esistente del blog — incluso l'encoder Transformer nella modellazione dello spread — usa un semplice embedding posizionale appreso. Le codifiche a tempo continuo, lo stato latente ODE-RNN e l'attenzione pesata sugli eventi sono tre modi per affrontare il problema.
Manca però qualsiasi evidenza che risolverlo sia importante. Tutte e tre sono al momento proposte, non risultati. La misura decisiva è un'ablazione a tre varianti su encoder e split con purging fissi, con F1 per classe, intervallo di confidenza e scale temporali apprese — e non è ancora stata eseguita.
Autori
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.