📝

Draft article

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

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

Trade Classification When You Have No Side Flag: Tick, Quote, Lee-Ready, BVC

Trade Classification When You Have No Side Flag: Tick, Quote, Lee-Ready, BVC
#microstructure
#trade-classification
#Lee-Ready
#order-flow
#deep-learning

Начнем с того, что отбросим версию этой проблемы, с которой открывается большинство учебников по микроструктуре. На площадках, где этот блог действительно торгует, направление сделки не нужно выводить — это готовое поле. Binance aggTrades передает isBuyerMaker, а поток сделок Bybit — side. Статья о построении баров берет сторону агрессора прямо из этого флага и не запускает классификатор:

def on_trade(self, timestamp: int, price: float, qty: float, is_buyer_maker: bool):
    side = -1 if is_buyer_maker else 1

Это бесплатная истина с наносекундным разрешением. Если вы строите подписанный order flow по современному потоку криптовалютной CEX, эта статья вам не нужна.

Это необходимо в четырех ситуациях, и они достаточно распространены, чтобы иметь значение:

  1. У вас есть только бары. Исторический OHLCV продает большинство поставщиков и использует большинство бэктестов; поток сделок не архивировался или стоит дороже, чем оправдывает исследование. Это описанная ниже проблема BVC.
  2. Площадка не помечает сторону. Небольшие CEX, наборы данных в стиле equity TAQ и почти все архивы до 2017 года дают только цену и объем.
  3. DEX и ончейн-поток. Событие swap сообщает, какой токен и куда переместился, но для определения агрессии в маршрутизированной многошаговой сделке или у агрегатора не остается флага, на который можно опереться.
  4. Вы воспроизводите опубликованное исследование. Оценки лямбды Кайла, VPIN и разложения спреда — почти вся литература по микроструктуре построена на данных, классифицированных Lee-Ready. Для сопоставимости нужно запустить их классификатор, а не прочитать настоящую сторону сделки.

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

Тиковый тест

Отметьте последовательность тестовых событий

Сравните текущую цену сделки PtP_t с предыдущей:

dttick={+1if Pt>Pt11if Pt<Pt1dt1tickif Pt=Pt1d_t^{\text{tick}} = \begin{cases} +1 & \text{if } P_t > P_{t-1} \\ -1 & \text{if } P_t < P_{t-1} \\ d_{t-1}^{\text{tick}} & \text{if } P_t = P_{t-1} \end{cases}

Интуиция проста: сделки, инициированные покупателем, поднимают ask и толкают цену вверх; сделки, инициированные продавцом, бьют в bid и толкают ее вниз. Нулевые тики переносят предыдущий знак, и именно здесь возникает большая часть ошибок: на ликвидном инструменте со спредом в один тик большинство последовательных сделок проходит по одной цене.

sign = np.sign(np.diff(prices, prepend=prices[0]))
sign[sign == 0] = np.nan
directions = pd.Series(sign).ffill().fillna(1).astype(int).values

Рабочая реализация с той же семантикой переноса уже поставляется как _tick_sign() внутри генераторов тиковых и объемных баров дисбаланса в статье За пределами временных баров. Используйте ее вместо поддержки второй копии.

Правило котировки

Сделки, расположенные внутри геометрии спроса и предложения

Вместо сравнения последовательных сделок сравните цену сделки с текущей средней котировкой Mt=(At+Bt)/2M_t = (A_t + B_t)/2:

dtquote={+1if Pt>Mt1if Pt<Mtundefinedif Pt=Mtd_t^{\text{quote}} = \begin{cases} +1 & \text{if } P_t > M_t \\ -1 & \text{if } P_t < M_t \\ \text{undefined} & \text{if } P_t = M_t \end{cases}
def quote_rule(trade_prices: np.ndarray,
               bid_prices: np.ndarray,
               ask_prices: np.ndarray) -> np.ndarray:
    """Classify trades using the quote rule.

    Returns +1 (buy), -1 (sell), or 0 (unclassified) for midpoint trades.
    """
    midpoints = (bid_prices + ask_prices) / 2.0
    directions = np.zeros(len(trade_prices), dtype=int)
    directions[trade_prices > midpoints] = 1
    directions[trade_prices < midpoints] = -1
    return directions

Правило котировки использует больше информации, чем тиковый тест, но ошибается в другом месте: оно ничего не говорит о сделках, прошедших точно по mid. На инструменте с узким тик-ограниченным спредом это не крайний случай, а значительная часть ленты. Правилу нужен запасной вариант — им и становится Lee-Ready.

Lee-Ready: гибрид и предположение об устаревших котировках

Торговый поток и поверхность отложенной котировки

Lee и Ready (1991) объединили два подхода: применять правило котировки, а для сделок по mid возвращаться к тиковому тесту.

dtLR={dtquoteif PtMtdttickif Pt=Mtd_t^{\text{LR}} = \begin{cases} d_t^{\text{quote}} & \text{if } P_t \neq M_t \\ d_t^{\text{tick}} & \text{if } P_t = M_t \end{cases}
def lee_ready(trade_prices: np.ndarray,
              bid_prices: np.ndarray,
              ask_prices: np.ndarray,
              quote_lag: int = 0) -> np.ndarray:
    """Lee-Ready classification.

    quote_lag : number of quote observations to lag (0 = contemporaneous).
                Set > 0 only if your quote feed is known to trail the trade
                feed; see the discussion of the 5-second rule below.

    Depends on tick_test() with carry-forward semantics — the `_tick_sign()`
    implementation from /blog/beyond-time-bars-candle-construction.
    """
    if quote_lag > 0:
        bid = np.concatenate([bid_prices[:quote_lag], bid_prices[:-quote_lag]])
        ask = np.concatenate([ask_prices[:quote_lag], ask_prices[:-quote_lag]])
    else:
        bid, ask = bid_prices, ask_prices

    directions = quote_rule(trade_prices, bid, ask)

    unclassified = directions == 0
    if unclassified.any():
        directions[unclassified] = tick_test(trade_prices)[unclassified]

    return directions

Самая интересная часть статьи обычно воспроизводится без объяснения. Lee и Ready не просто предложили гибрид: они предложили сравнивать каждую сделку с котировкой, действовавшей пять секунд назад. Это не модельный выбор о рынках, а поправка на артефакт передачи данных: на консолидированной ленте NYSE 1980-х сделки и котировки распространялись по разным каналам с разными задержками, причем котировки часто приходили раньше относящихся к ним сделок. Поэтому сравнение сделки с напечатанной одновременно котировкой означало сравнение с котировкой, уже отражавшей эту сделку, и переворачивало знак именно для сделок, сдвинувших рынок.

Для любого, кто переносит это на криптовалюту, следуют две вещи.

Правило 5 секунд устарело, и его применение вредно. Оно исправляет задержку передачи, которой больше нет. Рынки акций устранили этот разрыв к середине 1990-х; потоки сделок и дельты стакана криптоплощадки используют общее пространство последовательностей и получают отметки от одного matching engine с микросекундным разрешением. Пять секунд на BTCUSDT — не допуск синхронизации, а сотни обновлений стакана. Задержка котировок на пять секунд не исправляет устаревшие котировки, а создает их. Используйте quote_lag=0, если только не измерили реальное смещение в собственной записи.

Но сама проблема сохранилась в новой форме. Теперь устаревание котировок — ошибка вашего конвейера, а не биржи. Восстановите стакан из WebSocket-потока дельт, соедините сделки по настенному времени — и внесете задержку, знак и величина которой зависят от буферизации, политики повторной синхронизации снимка и часов. Измерьте ее до того, как доверять классификатору, читающему стакан: воспроизведите окно, соедините сделки с состоянием стакана и проверьте, какая доля выходит за пределы объединенного [bid, ask]. Сделка выше объединенного ask доказывает, что сопоставленный с ней стакан устарел.

Исправление отличается от решения Lee и Ready, но режим ошибки тот же.

Классификация совокупного объема

Кластеры объемов по классам направленности

Easley, Lopez de Prado и O'Hara (2012) решают другую проблему: что, если отдельных сделок вы не видите вовсе? BVC классифицирует агрегированный объем внутри бара, разделяя его с помощью стандартной нормальной CDF стандартизированного изменения цены:

Vtbuy=VtΦ(ΔPtσΔP),Vtsell=VtVtbuyV_t^{\text{buy}} = V_t \cdot \Phi\left(\frac{\Delta P_t}{\sigma_{\Delta P}}\right), \qquad V_t^{\text{sell}} = V_t - V_t^{\text{buy}}

где ΔPt=PtclosePtopen\Delta P_t = P_t^{\text{close}} - P_t^{\text{open}} и σΔP\sigma_{\Delta P} — стандартное отклонение изменения цены бара.

from scipy.stats import norm

def bulk_volume_classification(open_prices: np.ndarray,
                               close_prices: np.ndarray,
                               volumes: np.ndarray,
                               sigma: float | None = None) -> tuple:
    """Split bar volume into buy/sell using the BVC rule.

    Returns (buy_volume, sell_volume) as continuous, not integer, splits.
    """
    delta_p = close_prices - open_prices

    if sigma is None:
        sigma = max(float(np.std(delta_p)), 1e-8)

    buy_fraction = norm.cdf(delta_p / sigma)
    return volumes * buy_fraction, volumes * (1.0 - buy_fraction)

BVC — другой инструмент, а не худший вариант, и это объясняют три его свойства. Он не классифицирует сделки, а выдает долю: бар, закрывшийся без изменения, получает разделение 50/50, а не случайный знак для каждой сделки внутри него. Если потребителю нужен знак каждой сделки, BVC его дать не может. Он работает на барах, а не на ленте, и в этом весь смысл: это единственный метод здесь, работающий без данных уровня отдельных сделок. Наконец, σ\sigma — свободный параметр, меняющий результат: оценка по полной выборке создает заглядывание в будущее, а короткая скользящая оценка заставляет Φ\Phi насыщаться и приближает каждый бар к 0/100. Здесь нужна проверка чувствительности, а не значение по умолчанию.

В оригинальной статье сообщается, что BVC достаточно близко отслеживает классификацию на уровне сделок для оценки VPIN. Конкретная accuracy сильно зависит от размера бара, инструмента и оценщика σ\sigma, а вторичные источники редко называют все три. Не переносите результат исследования акций — измерьте его.

Единственный специфичный для классификации признак

Выделенный сигнал микроструктуры

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

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

relative_pricet=PtBtAtBt\text{relative\_price}_t = \frac{P_t - B_t}{A_t - B_t}

Сделка по bid отображается в 0, по ask — в 1, по mid — в 0,5. Это непрерывное обобщение правила котировки: вместо порога в mid модель получает расстояние и может выучить, что trade по 0,95 почти наверняка является покупкой, а trade по 0,55 малоинформативен. Значения вне [0,1][0, 1] — диагностический признак устаревшей котировки из предыдущего раздела; их стоит логировать, а не обрезать.

Для boosted baseline на этих признаках используйте конвейер из статьи о моделировании спреда с помощью машинного обучения, заменив objective на бинарный и, что особенно важно, сохранив purged и embargoed walk-forward CV. Не оценивайте микроструктурный классификатор обычным TimeSeriesSplit: эта статья и walk-forward optimization объясняют, почему полученное число будет оптимистичным. Для sequence-моделей на снимках стакана эталоном остается архитектура DeepLOB, и раздел об обучении применяется без изменений.

Как измерять правильно: криптовалюта дает метки

Помеченный поток данных крипторынка

Каждая оценка accuracy в литературе о классификации сделок на фондовом рынке сравнивается с прокси истины — TORQ, подвыборками аудиторского журнала или внутренними записями одной площадки, — поскольку настоящая сторона агрессора никогда не публиковалась. Криптовалюта ее публикует. isBuyerMaker в Binance aggTrades — бесплатная метка для каждой сделки. Поэтому этот эксперимент доступен всем, но опубликованного варианта для криптовалюты я не нашел. Протокол:

Данные. Месяц aggTrades для BTCUSDT плюс поток book ticker, объединенные так, чтобы каждая сделка несла bid и ask, действовавшие непосредственно перед ней. Повторите эксперимент на малоликвидном альткоине: вопрос как раз в том, что происходит при широком спреде.

Метка. direction = -1 if is_buyer_maker else +1. Затем отбросьте флаг и запустите каждый метод только на данных о ценах и котировках.

Методы. Тиковый тест, правило котировки, Lee-Ready с quote_lag=0, Lee-Ready с исторической задержкой 5 секунд (чтобы измерить ущерб устаревшего правила) и BVC с несколькими размерами баров в сравнении с настоящей долей объема покупок в баре.

Разрезы. Общая accuracy — наименее интересное число. Разбейте результат по режиму спреда (один тик против широкого: carry-forward тикового теста должен рушиться там, где спред зафиксирован), по тому, прошла ли сделка по mid (это изолирует место, где правило котировки воздерживается, а fallback Lee-Ready выполняет всю работу), по размеру сделки (aggTrades содержат sweep'ы; крупные сделки создают impact и дороже всего обходятся при неверном знаке) и по режиму волатильности (в BVC оценивается σ\sigma, поэтому его ошибка по построению зависит от режима).

Метод В целом По mid Узкий спред Широкий спред Крупные сделки
Тиковый тест
Правило котировки
Lee-Ready (lag 0)
Lee-Ready (lag 5 с)
BVC (доля бара) н/д н/д

Accuracy не является целью

Граница классификации и влияние на выполнение

Следуя дисциплине постановки задачи из DeepLOB, accuracy классификации — не результат, а вход для результата. Никто не торгует знаком отдельной сделки: торгуют величиной, вычисленной по целому потоку знаков, и ошибки не передаются в нее нейтрально. Классификатор, случайно ошибающийся в 10% сделок, и классификатор, систематически ошибающийся в 10% крупных сделок, двигающих цену, создают совершенно разный подписанный order flow при одинаковой accuracy. Ошибка carry-forward тикового теста особенно связана с неизменной ценой — режимом, в котором подписанный поток должен быть информативен. Поэтому настоящий результат дают приведенные выше разрезы, а не заголовочное число.

С настоящими метками следующий вопрос измеряется напрямую: оцените нужную величину по настоящим знакам, переоцените ее по знакам каждого классификатора и сообщите разницу.

  • Ценовой impact. Регрессируйте изменения mid-price на подписанный net taker flow по фиксированным окнам. Оценка уже задана в моделях издержек проскальзывания, а рабочая реализация на открытых данных Binance приведена в статье об оптимальном исполнении Almgren–Chriss. Не переопределяйте ее здесь: запустите дважды и сравните оцененный коэффициент.
  • Order flow imbalance как предиктор. Посчитайте корреляцию OFI с будущими доходностями для настоящих и классифицированных знаков. Падение этой корреляции и есть стоимость ошибочной классификации, выраженная в единственной важной единице.
  • Метрики токсичности. VPIN и признаки дисбаланса потока сделок используют классифицированный объем; оба описаны в статье о моделировании спреда с помощью ML. Их чувствительность к ошибке классификации измеряется тем же способом.

Практический вывод

Разрешенная карта потока заказов

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

Если нет, используйте Lee-Ready с актуальными котировками. Не потому, что он точен, а потому, что это стандарт, а сопоставимость с опубликованными оценками за три десятилетия ценнее нескольких непроверяемых пунктов accuracy. Правило пяти секунд — ископаемое эпохи задержек ленты 1980-х; воспроизведение его на современных данных добавляет именно ту устарелость, которую оно должно было устранить. Если есть только бары, BVC — единственный вообще работающий вариант, но он отвечает на другой вопрос: разделяет объем, а не выдает знак сделки.

В любом случае сравнивайте методы с бесплатными метками криптовалюты, разбивайте результат по режиму спреда и размеру сделки и сообщайте, во что ошибка обходится downstream-оценкам, а не только какой процент оказался правильным.

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

Авторы

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-трейдингу и обновления платформы.

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