Баг таймаута: почему три позиции пользователей провисели 33 часа
Подписчик задал простой вопрос: «почему эти позиции на Binance всё ещё открыты через два дня?». Спека стратегии задавала максимальное время холда сильно ниже этого. Позиции были открыты 33 часа. Честный post-mortem бага, фикса и того, чего это стоило нам в доверии, без раскрытия того, как устроена базовая модель, потому что эта часть не для публикации.
Что мы скажем, а что нет
Модель сигналов за сделками (проприетарный внутренний квант + AI-ресёрч фирмы) здесь не описывается. Инпуты, скоринг и максимальный холд, который задаёт спека, намеренно не раскрываются. Что действительно на столе в этом посте: путь кода на слое исполнения, баг и фикс. Эта часть — воспроизводимая инженерия, а не эдж.
Обнаружение
Три лонговые позиции на аккаунте подписчика, открытые в одном и том же утреннем окне. Спека стратегии задаёт максимальное время холда, после которого позиция принудительно закрывается по рынку независимо от P&L (потому что базовый тезис затухает за этой точкой; по замыслу этот горизонт остаётся внутренним).
Клиент заметил, что позиции всё ещё висят на Binance спустя долгое время после запланированного закрытия. Он начал разговор словами «почему они ещё открыты?». Это ровно тот момент, когда квант-фирма приобретает или теряет клиента. Будь мы из тех, кто отмахивается и валит вину на Binance, мы были бы совсем другой конторой. Мы стали разбираться.
Механизм
Поток исполнения, когда срабатывает сигнал:
- Детектор выдаёт сигнал с монетой, скором, стороной и предлагаемым входом
- Executor раскидывает его на (a) аккаунт фирмы, (b) всех Pro-подписчиков с включённой этой стратегией
- Для каждой цели раскидки ставит рыночный вход + аварийный SL на Binance
- Регистрирует позицию в positionMonitor под таймаут, специфичный для стратегии
Шаг 4 — там, где сидел баг. Executor обрабатывал сделки фирмы и сделки пользователей на разных путях кода: сделки фирмы получали регистрацию в positionMonitor; сделки пользователей просто помечались как status=filled. Для пользовательских аккаунтов регистрации таймаута не было. Binance держала ордер SL, но если цена так и не доходила ни до SL, ни до какого-либо TP, позиция висела бы там бесконечно.
Как это дожило до продакшна
Изначальный путь исполнения был только для фирмы. Когда два месяца назад мы добавили исполнение по пользователям, мы форкнули путь, но не пробросили логику регистрации таймаута. Юнит-тесты, которые мы написали для пользовательского исполнения, покрывали «встал ли ордер правильно» и «зарегистрировался ли SL на Binance», но не «срабатывает ли таймаут из спеки», потому что осмысленный тест на это требует часов реального времени на прогон от начала до конца, и мы опёрлись на продакшн фирмы как на страховку.
Продакшн фирмы ни разу не спровоцировал баг, потому что позиции фирмы всегда закрывались через SL или TP в пределах своего окна, по случайности. Впервые условия сложились так, чтобы удержать позицию живой дольше таймаута из спеки, на пользовательском аккаунте: без регистрации, в сессии, где цена осталась в диапазоне, не задев ни SL, ни TP.
Фикс
Мы добавили карту холда по стратегиям в userPositionGuard.js с проверкой принудительного закрытия на каждом тике. Реальные значения холда — часть спеки стратегии и остаются приватными. Механизм простой: каждые 60 секунд guard сканирует открытые пользовательские сделки, сравнивает их возраст с максимумом из спеки стратегии и принудительно закрывает рыночным reduceOnly-ордером, когда порог перейдён.
Задеплоили меньше чем за 20 минут после сообщения клиента. Следующий тик поймал все три застрявшие позиции и закрыл их по рынку. Чистый P&L по трём вышел почти в ноль: долларовая цена бага была символической. Реальной ценой была цена доверия.
Реальная цена
Подписчик, который платит за систему с заданным максимальным холдом, заслуживает систему, которая этот холд соблюдает, а не уезжает на 33 часа за него. Мы рассказали клиенту ровно, что произошло, задеплоили фикс, пока он ещё был на линии, и показали ему закрытые записи с посчитанным PnL, как только они появились.
Клиент сохранил подписку. Это правильный исход только потому, что мы поймали баг спустя считанные часы после сообщения. Если бы тот же баг ударил во время рыночного краха и позиция просидела бы просадку −25%, потому что ни один таймаут не сработал, разговор был бы совсем другим.
Что мы поменяли помимо фикса
- Паритет тестов между путями исполнения фирмы и пользователя. Любая фича, добавленная в исполнение фирмы, должна иметь эквивалентный тест в пользовательском исполнении. Хватит опираться на «продакшн фирмы ловит всё».
- Расчёт PnL при внешних закрытиях. Пока дебажили, мы обнаружили, что когда Binance закрывала позицию извне (исполнение SL, ручное закрытие), мы помечали её как status=closed, но не считали pnlPct/pnlUsd. Поправили в том же коммите. Теперь у каждой закрытой сделки есть полные данные P&L независимо от того, кто инициировал закрытие.
Почему мы это публикуем
Баг паритета «фирма против пользователя» в исполнении — ровно то, что другие операторы крипто-ботов прятали бы. Большинство платформ работают на закрытом коде, непрозрачной атрибуции P&L и «доверьтесь нам, система работает как заявлено». Когда что-то ломается, расплачиваются клиенты.
Мы предпочитаем опубликовать баг, фикс и урок. Сама модель остаётся в сейфе: эта часть — IP фирмы и причина, по которой сервис вообще существует. Но инженерные леса вокруг неё должны быть доступны для проверки, а когда они ломаются, post-mortem должен быть публичным. Это стандарт, которому мы следуем сами.
Исполнение продакшн-уровня, с доказательствами на руках
Hedonist Pro автоматически исполняет наши проприетарные сигнальные алгоритмы на вашем аккаунте Binance. Модели остаются приватными; слой исполнения проходит публичный post-mortem, когда что-то идёт не так.
Смотреть тарифы Pro →