📝

Draft article

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

← К списку статей
August 22, 2026
5 мин. чтения

Irregular Time in Tick Models: Continuous-Time Encodings vs. Plain Positional Embeddings

Irregular Time in Tick Models: Continuous-Time Encodings vs. Plain Positional Embeddings
#HFT
#tick-data
#deep-learning
#high-frequency
#prediction

Временные бары — произвольное сжатие, разрушающее время событий, поэтому границы бара должна задавать активность. Этот аргумент подробно разобран в статье Типы баров и методы агрегации для алгоритмической торговли, где приведены 17 типов баров и рабочие генераторы. Эта статья начинается на шаг позже и рассматривает то, что пока не решено: даже после перехода к тиковым, объемным барам или барам дисбаланса модель, которой они подаются, все еще предполагает регулярные интервалы. nn.TransformerEncoder с обучаемым позиционным вложением воспринимает позицию 7 как "седьмой слот" независимо от того, пришел ли этот тик через 200 микросекунд или через 40 секунд после позиции 6. Время между событиями — именно то, что должны сохранять бары активности, — снова теряется на входном слое.

Итак, вопрос этой статьи узок и проверяем: как сообщить модели последовательностей, когда именно произошел тик, и превосходит ли сложный ответ тривиальный?

Необходимый фон

Нерегулярные импульсы событий

Ниже перечислены предпосылки. Все они уже разобраны в этом блоге и здесь не выводятся заново:

  • Микроструктурный шум и отскок bid-ask. Неявная модель спреда Ролла выводит отрицательную серийную ковариацию тиковых доходностей из первых принципов, в единицах цены, и указывает на распространенную ошибку реализации: Моделирование bid-ask спреда с помощью ML.
  • Признаки стакана. Дисбаланс стакана, взвешенная средняя цена, отношение глубины на уровнях 1–5, давление стакана и отношение спреда к тику сведены в той же статье; выводы для OBI и средневзвешенной по объему цены, включая оговорку, что взвешенная средняя цена не является микроценой Стоикова, приведены в DeepLOB: глубокое обучение на лимитных книгах заявок.
  • Поток ордеров и интенсивность сделок. Дисбаланс сделок, VPIN и лямбда Кайла разобраны в статье о моделировании спреда; интервал между ордерами и интенсивность самовозбуждения Хоукса как временные признаки приведены в Цифровой отпечаток: идентификация трейдера. Грубая интенсивность 1/dt, которую первоначально предлагал этот черновик, является более слабой версией формулировки Хоукса.
  • Внутридневная нестационарность. U-образный профиль объема, расширение спреда в спокойные периоды и sin/cos-кодирование времени суток рассмотрены в статье о моделировании спреда как признаки рыночного режима.
  • Метки. Оба соглашения о сглаживании (будущее среднее против текущей цены и будущее среднее против предыдущего среднего), трехклассовая дискретизация с порогом α\alpha и предупреждение LOBFrame о чувствительности меток к α\alpha и kk приведены в статье DeepLOB.
  • Дисбаланс классов. При мертвой зоне доминирует класс FLAT, поэтому отчетной метрикой должен быть F1, а не accuracy, как объясняется в той же статье.
  • Длительность формирования бара. Настенные часы, за которые заполняется объемный бар, дают полезный признак для тиковой модели; генераторы для его получения описаны в Типах баров и методах агрегации.

Важно сказать прямо, потому что первоначальная версия этого черновика ошибалась: рост точности направления с 50,0% до 50,5% не означает автоматически прибыльность. Центральная мысль LOBFrame, обсуждаемая в статье DeepLOB, состоит в том, что прогнозируемое движение должно быть достаточно большим, чтобы покрыть спред, прежде чем точность начнет что-либо значить. Честный отрицательный результат в этом блоге показывает, что происходит, если предположить обратное.

Базовая модель

Базовое последовательное кодирование событий

Пусть базовым кодировщиком будет CNN-Inception-LSTM в стиле DeepLOB; архитектура, проверенные числа для FI-2010 Setup 2 (F1 83,40 / accuracy 84,47 при k=10k=10), оговорки LOBFrame и рабочая повторная реализация на PyTorch приведены в DeepLOB: глубокое обучение на лимитных книгах заявок. Все дальнейшее оставляет этот кодировщик неизменным: вопрос только в том, что добавить к его входному представлению.

Три способа кодировать нерегулярное время

Альтернативные нерегулярные кодировки времени

Вариант A: delta_t как обычный признак

Тривиальный ответ: добавить Δti=titi1\Delta t_i = t_i - t_{i-1} (логарифмически преобразованный и приведенный к z-оценке) еще одним столбцом вектора признаков рядом с 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)

Наиболее принципиальный вариант рассматривает латентное состояние как непрерывный процесс, моделируемый нейронным ОДУ (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 акций по цене bid обычна. Сделка, которая сметает три ценовых уровня, означает смену режима. Этот априорный сигнал можно напрямую добавить в механизм внимания. Определим оценку важности события:

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:

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 for Trading. Специфичен для тиков только набор горизонтов: 1, 10, 50 и 100 событий, а не дней. Поэтому во время всплесков эти горизонты сильно перекрываются по времени, а в спокойные периоды почти не перекрываются — с такой сложностью литература о дневных горизонтах не сталкивается.

Очистку нужно измерять в событиях

Интервал исключения пространства событий

Очищенная walk-forward-валидация подробно разобрана в статье Walk-Forward Optimization (якорная, скользящая и комбинаторная очищенная CV, WFER, скорость деградации), а рабочая purged_walk_forward() с явными зазорами purge и embargo приведена в статье о моделировании спреда. Таксономия смещения заглядывания в будущее количественно показывает, как утечки такого рода влияют на заявленный Sharpe.

Специфика тиковых данных в том, что соседние тики, разделенные миллисекундами, почти дублируют друг друга. Поэтому зазор очистки размером в дни бессмыслен, если один всплеск помещает 500 почти одинаковых выборок в одну секунду. Зазор нужно задавать в событиях, а его правильный размер — эмпирический вопрос о структуре всплесков конкретного инструмента, а не константа, которую можно скопировать из статьи.

Это утверждение легко проверить: переберите зазор очистки в событиях и постройте зависимость валидационного F1 от него. Если F1 падает при увеличении зазора, а затем выходит на плато, точка плато и есть нужный зазор; если F1 вообще не падает, утечка между соседними тиками была не той проблемой, о которой вы думали.

Эксперимент, который даст ответ

Оценка контролируемой временной модели

Все вышеописанное относится к архитектуре. Ничто из этого не является доказательством. Статья не соответствует планке этого блога, пока не будут выполнены следующие прогоны:

Настройка. Зафиксируйте один набор данных (сделки BTC/USDT — основной набор данных), один кодировщик, одно определение метки и одно очищенное разбиение. Меняйте ровно один фактор.

Три варианта.

Вариант Информация о времени Позиционное кодирование
A Δt\Delta t как необработанный входной признак обычное обучаемое позиционное вложение
B нет, кроме кодирования ContinuousTimeEncoding (обучаемые временные масштабы)
C Δt\Delta t как признак и кодирование непрерывного времени ContinuousTimeEncoding

Что сообщить.

  1. F1 по каждому классу, а не accuracy: при метке с мертвой зоной доминирует FLAT, поэтому accuracy неинформативна ровно по причине, описанной в статье DeepLOB.
  2. Доверительный интервал по повторным прогонам с разными seed. Разница F1 в 0,4 пункта между вариантами ничего не значит без него.
  3. Обученные временные масштабы. После обучения выведите torch.exp(model.time_encoding.log_timescales). К каким значениям они придут — самое интересное число эксперимента, и для его получения нужна одна строка.
  4. Аппаратная стоимость и время на часах для каждого варианта в стиле Налог на IPC: измеряйте на раскрытом оборудовании и берите медиану по N прогонам. Если вариант B в 3 раза увеличивает время инференса по сравнению с A ради доли пункта F1, это и есть ответ.

Сообщите об отрицательном результате, если он есть. "На 30 днях тиков BTC кодирование непрерывного времени ничего не дает по сравнению с подачей Δt\Delta t как признака" — более полезный пост, чем обзор трех архитектур, которые никто не измерял. Это также согласуется с общим выводом блога: тщательно сконструированные преимущества обычно испаряются при честной валидации.

Текущее состояние

Путь непрерывного исследования времени

Открытый вопрос реален: модели последовательностей, получающие события через нерегулярные интервалы, по-прежнему кодируют позицию, а не время. Существующие материалы блога, включая Transformer-кодировщик в статье о моделировании спреда, используют обычное обучаемое позиционное вложение. Непрерывное кодирование, латентное состояние ODE-RNN и внимание с весами событий — три способа это исправить.

Не хватает доказательств того, что это исправление вообще важно. Сейчас все три подхода — темы, а не результаты. Решающее измерение — абляция трех вариантов на фиксированном кодировщике и фиксированном очищенном разбиении с отчетом F1 по классам, доверительного интервала и обученных временных масштабов. Такой эксперимент еще не проводился.

Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

Авторы

Eugen Soloviov
Eugen Soloviov

Инженер торговых систем

Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.

Newsletter

Будьте в курсе событий

Подпишитесь на нашу рассылку, чтобы получать эксклюзивную аналитику по AI-трейдингу и обновления платформы.

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.