Подтверждение приёма — это не исполнение: машина состояний ордера
Биржа сообщила, что получила ваш ордер, и биржа сообщила, что он исполнился, — это разные события. Боты, которые их смешивают, показывают прибыль, которой не было.
Инженеры Vizanix · об авторе
- РАЗДЕЛ
- Архитектура ботов
- ОПУБЛИКОВАНО
- 2026-08-28
- ГЛАВ
- 6
- ЧИТАТЬ ДАЛЬШЕ
- 3
- ЯЗЫК
- написано на русском
Самая значимая строчка в слое исполнения — та, что решает, что значит «ордер прошёл». Ошибитесь в ней, и всё дальше по течению — учёт позиции, риск, P&L, бэктест, с которым вы сравниваете, — тихо стоит на ложной посылке.
Три разные истины
- HTTP-ответ вернул 200. Ваш запрос дошёл до площадки и принят к обработке. Ничего не исполнилось.
- Ордер появился в стакане. Он живой и висит. По-прежнему ничего не исполнилось.
- По приватному потоку пришёл отчёт об исполнении. Что-то исполнилось: конкретный объём по конкретной цене.
Только третье меняет вашу позицию. В нашем микроструктурном движке это записано в проект как правило: подтверждение приёма — не исполнение, а исполнения берутся из приватного потока ордеров и сделок, но никогда — из ответа на запрос постановки.
Состояния, которые действительно нужны
class OrderState(Enum):
PENDING = auto() # запрос собран, ещё не отправлен
SENT = auto() # в полёте — исход честно неизвестен
ACKED = auto() # площадка приняла; исполнено ноль
PARTIAL = auto() # часть объёма исполнена, остаток живой
FILLED = auto() # исполнен полностью
CANCELLED = auto() # живого остатка нет; частичные исполнения могли быть
REJECTED = auto() # площадка отказала; ничего не исполнилось
UNKNOWN = auto() # таймаут или обрыв — разрешается только запросомUNKNOWN — состояние, которое чаще всего пропускают, и именно оно важно. Таймаут не равен отказу. Ордер может быть живым. Он может быть исполнен. Считать таймаут неудачей и переотправлять — классический способ получить две позиции.
Разрешение неизвестности
Именно поэтому каждый запрос, меняющий состояние, несёт клиентский ID ордера, сгенерированный до первой попытки и переиспользуемый во всех повторах. Это даёт две вещи: площадка отклонит настоящий дубль вместо его исполнения, и у вас есть ключ, по которому можно спросить, что произошло.
async def place(intent) -> Order:
coid = client_order_id(intent) # детерминированный, генерируется один раз
try:
resp = await api.create_order(intent, client_order_id=coid)
return Order(coid, OrderState.ACKED, resp)
except (Timeout, ConnectionError):
# Мы НЕ знаем, дошло ли до площадки. Спрашиваем.
return await resolve_unknown(coid)
async def resolve_unknown(coid) -> Order:
for delay in (0.2, 0.5, 1.0, 2.0, 5.0):
await asyncio.sleep(delay)
found = await api.query_order(client_order_id=coid)
if found:
return Order.from_venue(found)
alert("ордер %s не разрешён — останавливаю символ", coid)
raise Unresolved(coid)Последние две строки — то, что обычно опускают. Если состояние разрешить не удалось, правильное поведение — прекратить торговать этим символом и позвать человека. Бот, который продолжает с неразрешённым висящим ордером, гадает о собственной позиции.
Отмена — тоже изменение состояния
К отменам отношение такое же. Запрос на отмену, ушедший в таймаут, оставляет ордер в UNKNOWN, а не в CANCELLED. Оптимистично пометить его отменённым — это то, как бот получает висящий ордер, которого, по его мнению, не существует; ордер затем исполняется в наихудший момент в позицию, которой никто не управляет.
И отмена может проиграть гонку исполнению. Ордер способен исполниться в те микросекунды, что лежат между вашим решением отменить и обработкой отмены площадкой. Машина состояний обязана принять отчёт об исполнении по ордеру, который она отменяла, и считать исполнение главнее.
Сверка как страховка
Машины состояний расходятся с реальностью. Потоки теряют сообщения. Процессы перезапускаются. Единственная надёжная коррекция — периодическая сверка с площадкой: забрать открытые ордера и позиции, сравнить с локальным состоянием и разрешить каждое расхождение в пользу площадки.
Запускайте после каждого переподключения, после каждого перезапуска и по медленному таймеру как страховку. Логируйте найденные расхождения, а не чините их молча: сверка, которая начала регулярно что-то находить, сообщает, что выше по течению что-то сломано.
Правило, сказанное прямо
- Позиция меняется только по отчётам об исполнении.
- Каждый запрос, меняющий состояние, несёт клиентский ID ордера.
- Таймаут означает
UNKNOWN, аUNKNOWNразрешается запросом. - Неразрешимое состояние останавливает символ и зовёт человека.
- Сверка с площадкой при переподключении, перезапуске и по таймеру.
Пять правил. Большинство багов исполнения, которые нас просят починить, — нарушение одного из них.
Статья описывает инженерную практику. Это не инвестиционная рекомендация. Vizanix разрабатывает программное обеспечение и не обещает торговую доходность.