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

Updating the Volume Curve Intraday: Does Adaptive Forecasting Actually Help?

Updating the Volume Curve Intraday: Does Adaptive Forecasting Actually Help?
#microstructure
#liquidity
#execution
#prediction
#order-book

В статье TWAP, VWAP и POV мы напрямую сравнили все три планировщика на 90 днях воспроизведения L2 для BTCUSDT и пришли к выводу, который намеренно оставили незажившей раной: в криптовалютах слабое место конвейера - кривая объема, а не нарезка ордера. В той статье мы намеренно выпустили простую версию - медианную кривую по дню недели и времени суток, переоцениваемую раз в неделю и неизменную в течение торгового дня. Она показала, что все преимущество VWAP над TWAP связано с качеством прогноза: при учете ошибки реализованной кривой объема VWAP опережает TWAP примерно на 4 б.п. в терцили дней с лучшим прогнозом, а в худшем терциле разница растворяется в шуме.

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

В этой статье реализован внутридневной обновитель, он запускается на том же стенде, а затем приводится дельта IS относительно статической кривой - с распределением и значением для последнего квартиля, а не только со средним.

Что проверяется

Адаптивное обновление априорной кривой объема по наблюдаемой торговой активности

Статическая кривая - это априорное распределение: фиксированный вектор u={u1,,uB}u = \{u_1, \ldots, u_B\} ожидаемых долей объема по корзинам, оцененный офлайн. Адаптивная версия рассматривает его как априорное распределение, которое можно обновлять. После того как прошло cc корзин и стали известны фактические объемы V1,,VcV_1, \ldots, V_c, появляются два новых фрагмента информации, отбрасываемых статической кривой:

  1. Оценка уровня. Если априорное распределение говорит, что на корзины 1..c1..c должна приходиться доля icui\sum_{i \le c} u_i дневного объема, а фактически они принесли icVi\sum_{i\le c} V_i единиц, то подразумеваемый дневной итог равен V^=icVi/icui\hat{V} = \sum_{i \le c} V_i \big/ \sum_{i \le c} u_i. Это сигнал "сегодня будет большой объем" / "сегодня будет мертвый рынок", доступный уже с первой корзины.
  2. Коррекция формы. Если реализованная форма прошедших корзин систематически отклоняется от априорной, оставшиеся корзины тоже могут отклониться - но только если ошибки внутридневной кривой объема автокоррелированы внутри дня. Это отдельный эмпирический вопрос, а не допущение, которое можно бесплатно принять.

Ниже реализация полностью использует (1) и совсем не использует (2): она перенормирует нетронутый хвост априорной кривой под обновленный итог. Это консервативная версия и правильный первый эксперимент: если одно лишь обновление уровня уже захватывает большую часть доступной дельты, механизм формы будет неоправданным усложнением.

import numpy as np
from typing import Optional


class AdaptiveVolumePredictor:
    """
    Bayesian-style adaptive volume profile predictor.

    Combines a prior (the offline day-of-week x time-of-day curve)
    with volume observed so far today to produce an updated forecast
    for the remaining buckets.
    """

    def __init__(self, historical_profiles: np.ndarray):
        """
        Args:
            historical_profiles: shape (n_days, n_buckets), each row sums to 1.0.
                Use the median-based, day-of-week-conditioned curve from the
                TWAP/VWAP/POV article -- a pooled mean curve is misspecified
                and will make the adaptive version look better than it is by
                giving it a weaker baseline to beat.
        """
        self.prior_profile = np.median(historical_profiles, axis=0)
        self.prior_profile /= self.prior_profile.sum()
        self.prior_iqr = np.subtract(*np.percentile(historical_profiles, [75, 25], axis=0))
        self.n_buckets = len(self.prior_profile)

    def predict(
        self,
        observed_volumes: np.ndarray,
        current_bucket: int,
        total_volume_estimate: Optional[float] = None,
    ) -> np.ndarray:
        """
        Predict absolute volume for every bucket: realized values for elapsed
        buckets, forecasts for the remainder.

        Args:
            observed_volumes: actual volumes in buckets 0..current_bucket-1
            current_bucket: index of the current bucket (0-based)
            total_volume_estimate: external ADV estimate (optional prior on level)
        """
        profile = np.zeros(self.n_buckets)

        if current_bucket == 0:
            base = total_volume_estimate if total_volume_estimate else 1.0
            return self.prior_profile * base

        profile[:current_bucket] = observed_volumes[:current_bucket]
        observed_total = observed_volumes[:current_bucket].sum()

        expected_fraction_so_far = self.prior_profile[:current_bucket].sum()
        if expected_fraction_so_far > 0.01:
            implied_total = observed_total / expected_fraction_so_far
        else:
            implied_total = observed_total * self.n_buckets

        if total_volume_estimate:
            w_obs = expected_fraction_so_far
            implied_total = w_obs * implied_total + (1 - w_obs) * total_volume_estimate

        remaining_prior = self.prior_profile[current_bucket:]
        remaining_sum = remaining_prior.sum()
        if remaining_sum > 0:
            remaining_volume = max(0.0, implied_total - observed_total)
            profile[current_bucket:] = remaining_prior / remaining_sum * remaining_volume

        return profile

В этом коде эксперимент определяют две детали. Априорная кривая - это медианная кривая с учетом дня недели, а не объединенное среднее. Почему это важно на рынке, где один каскад ликвидаций может дать 15% дневного объема за десять минут, объясняется в разделе о кривой объема статьи про VWAP. Обновление уровня сжато в сторону внешней оценки ADV с весом, равным прошедшей априорной доле, поскольку в первой корзине implied_total - одно наблюдение, деленное на число, близкое к нулю. Обновитель без сжатия хуже всего работает именно тогда, когда до конца дня еще больше всего времени для ущерба.

Стенд

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

Стенд идентичен запуску TWAP/VWAP/POV, поэтому числа можно сравнивать построчно:

  • Инструмент и данные: бессрочный BTCUSDT, 90 дней воспроизведения L2 (верхние 20 уровней, 100 мс) плюс лента сделок. Везде UTC; временные метки funding помечены.
  • Родительские ордера: 500 ордеров, горизонт T=4T = 4ч, размер 0,75% скользящего ADV за 30 дней, сторона покупки, цена решения - mid в начале. Время случайного старта то же, что в опубликованном запуске.
  • Варианты: (A) VWAP на статической кривой с еженедельной переоценкой - опубликованный baseline; (B) VWAP на том же априорном распределении с внутридневным обновлением; (C) TWAP как нижняя граница.
  • Метрики: IS в б.п. цены решения с учетом комиссий относительно момента прихода - среднее, медиана, стандартное отклонение, 95-й перцентиль и IS последнего квартиля каждого родительского ордера. Проскальзывание VWAP записывается только как диагностика, а не как таблица результатов; в разделе с benchmark статьи про VWAP показан пример, где проскальзывание VWAP и IS относительно прихода ранжируют два алгоритма в противоположном порядке. Полная декомпозиция IS, включая стоимость упущенной возможности, приведена в статье implementation shortfall и TCA.

Результаты

Помогает ли это там, где нужно?

Заголовочное среднее - наименее интересное число. Опубликованный результат установил, что разрыв VWAP-TWAP монотонен относительно ошибки реализованной кривой объема, поэтому адаптивный вариант нужно оценивать так же: разделить 90 дней на терцили по расстоянию L1L_1 между статическим прогнозом и реализованными объемами корзин, затем отчитаться о дельте IS "адаптивный минус статический" внутри каждого терцила.

Оба исхода можно публиковать, но означают они противоположное:

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

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

Наклон стакана: добавляет ли он что-то?

Абстрактный ландшафт глубины лимитного стакана и наклона ликвидности

Один признак из чернового набора признаков в этом блоге еще не рассматривался: наклон стакана - насколько быстро накапливается суммарная глубина по мере удаления от лучшей цены. Подгоните CumVol(d)=α+βd\text{CumVol}(d) = \alpha + \beta d по верхним KK уровням; большое β\beta означает, что глубина сосредоточена у лучшей цены, малое β\beta - что она тонко распределена по стакану.

def book_slope(prices: np.ndarray, volumes: np.ndarray, mid: float) -> float:
    """Slope of cumulative depth vs. distance from mid. One side only."""
    distances = np.abs(prices - mid)
    return float(np.polyfit(distances, np.cumsum(volumes), 1)[0])

Имеет смысл формулировать только измеренную версию этого утверждения: меняет ли добавление book_slope к существующему набору признаков прогноз объема или итоговый IS сверх baseline AR/EWMA? Статья о моделировании spread задает стандарт - сообщайте качество выше тривиального baseline с указанным горизонтом, поскольку в этих рядах доминирует persistence, а заголовочный R² в основном измеряет автокорреляцию.

Что эта статья намеренно не выводит заново

Целевой исследовательский модуль внутри ограниченного количественного конвейера

Все перечисленное ниже уже подробно разобрано в блоге, а повторное объяснение здесь создало бы лишь более слабую вторую версию:

  • Измерение ликвидности. Оценка Roll (с исправлением signed-root и ловушкой "единицы цены против доходностей") и lambda Кайла: моделирование spread с помощью машинного обучения. Lambda Кайла также откалибрована на реальных Binance aggTrades как коэффициент постоянного воздействия в статье Almgren-Chriss. Стоимость sweep по стакану: модели проскальзывания и затрат.
  • Признаки стакана. Взвешенный mid (который не является microprice Стоикова - тот скорректирован под мартингальность), многоуровневый дисбаланс стакана, OFI и полная таксономия признаков LOB: DeepLOB и моделирование spread.
  • Внутридневные паттерны. U-образная форма акций - неверная модель для рынка без закрытия; криптовалютная структура сессий, funding и недели, которая ее заменяет, вместе с сеткой "день недели × время суток" приведена в статье про VWAP. Циклическое кодирование времени суток есть в таблице признаков статьи о spread.
  • Запланированные события. Экспирации Deribit в 08:00 UTC, макроданные США в 12:30/14:00 UTC, расчеты CME - рассматривайте их как dummy-признаки, а не позволяйте им загрязнять baseline-кривую; конкретный криптокалендарь приведен в статье про VWAP. Приведенный выше адаптивный обновитель - не обработчик событий, и не следует требовать от него быть таковым.
  • Модели. Gradient boosting с purged и embargoed walk-forward CV (обычный TimeSeriesSplit протекает через перекрывающуюся цель forward-window) описан в моделировании spread; семейство CNN-LSTM, входной тензор 40 признаков/10 уровней, LOBFrame и обсуждение кризиса воспроизводимости - в DeepLOB. Важно: ось уровней нельзя схлопывать до рекуррентного слоя.
  • Решение по дочернему ордеру. Passive против aggressive - это вычисляемая точка безубыточности p=δ/(Π+δ)p^* = \delta/(\Pi + \delta), а не жестко заданный порог spread: тактики исполнения дочерних ордеров. Target по доле участия эндогенен - ваши собственные fills печатаются в ленте, а исправление приведено в статье про VWAP.
  • Фрагментация площадок. Умная маршрутизация ордеров в криптовалютах, с TCA-таблицей по каждой площадке в статье implementation shortfall.
  • Задержка в production. Раздел DeepLOB о production; обратите внимание, что статья о spread уже уточняет заявления о времени inference - LightGBM с 2000 раундами предсказывает из Python за десятки микросекунд, а однозначные микросекунды дает только скомпилированный predictor.

Adversarial dynamics заслуживают одного абзаца, а не отдельного раздела. Отображаемая глубина отчасти постановочна, а механика обнаружения - скорость отмен, скорость наращивания стены, поведение при приближении цены, layering - разобрана с конкретным методом на основе PIQ в статье анализ позиции в очереди и стен. Единственная добавка этой статьи могла бы состоять в использовании их как входов прогнозиста: отношение отмен к сделкам, среднее время нахождения у лучшей цены и доля глубины, отменяемая в течение 100 мс после появления, подаются в модель объема вместе с сигналом прошедшей корзины. Добавляют ли они качество поверх существующего набора признаков - открытый вопрос измерения, а не утверждение.

Заключение

Путь от неопределенности к измеренному результату исполнения

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

Эта статья не будет приписывать production-системе значение в б.п. без воспроизведения. Утверждения вида "хорошо настроенное прогнозирование ликвидности экономит 2-5 б.п., то есть меняет Sharpe с 1,5 до 2,0" - именно тот жанр, который дефлированный Sharpe и multiple testing призван разоблачать: без источника, без условий и без указания разброса. Важное здесь число - IS последнего квартиля в худшем терциле дней, и оно либо измерено, либо не приводится.

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

Авторы

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

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