Торговый бот в бою: запуск, API брокера и сбои - Tradeonia
TradeoniaTM
ГлавнаяСтатьиОт бэктеста к бою: как запустить торгового робота на реальном счёте

От бэктеста к бою: как запустить торгового робота на реальном счёте

Бэктест закончился, кривая красивая, код отлажен. Дальше начинается часть, о которой почти не пишут: между вашим решением и позицией появляются брокер, сеть и время. Разбираем, что именно ломается на этом участке, — на опыте собственного робота, который торгует на реальные деньги.

·24 минуты чтения·Редакция Tradeonia

Третья статья из серии об алготрейдинге и торговых роботах.

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

Чем бой отличается от бэктеста

В бэктесте вход в позицию — это строка вроде entry = bar.open. Присваивание. Оно не может не сработать, не может сработать наполовину и не может сработать дважды.

В бою та же строка превращается в разговор: бот отправляет запрос, ждёт ответа, получает или не получает его. И вот здесь возникает то, чего в бэктесте нет вообще.

В бэктесте у действия два исхода: получилось и не получилось. В бою — три: получилось, не получилось и «неизвестно». Третий исход — источник почти всех аварий живых роботов, и ниже мы разберём его отдельно.

Различий на самом деле три, и все растут из одного корня: бот перестал быть единственным участником процесса.

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

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

Появился отказ как штатное событие. Сеть рвётся, API отвечает ошибкой, брокер отклоняет заявку, биржа останавливает торги. В бэктесте таких веток кода нет за ненадобностью. В бою это половина программы — и именно эта половина никогда не проверялась на истории.

Если свести различия в таблицу, получится вот что:

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

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

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

Отсюда арифметика, к которой мы вернёмся в конце: если один отказ стоит месячного дохода, то надёжность — это не «инженерная гигиена», а вторая половина вашей доходности.


Бумажная торговля: что она проверяет, а что нет

Между бэктестом и живыми деньгами есть промежуточная ступень, и пропускать её не стоит. Называется она по-разному — бумажная торговля, paper trading, демо-счёт, песочница, — но означает одно: бот работает по-настоящему, а деньги ненастоящие.

Sandbox (песочница) — отдельный тестовый контур брокера, где заявки принимаются, «исполняются» и отражаются в портфеле, но на биржу не попадают. У большинства крупных российских брокеров с торговым API она есть.

Что бумажный режим действительно проверяет — и проверяет отлично:

  • код доходит до конца, а не падает на третьем часу;
  • авторизация, токены, форматы запросов и ответов;
  • бот правильно понимает расписание торгов, перерывы и календарь выходных;
  • состояние переживает ночь, перезапуск и обрыв связи;
  • логи содержат то, что нужно для разбора, а не то, что было удобно писать.

А теперь — чего он не проверяет, и это принципиально. Возьмём документацию песочницы Т-Инвестиций, там всё сказано прямо: рыночные заявки исполняются по цене последней сделки, а не по реальному стакану; «выставленные заявки не влияют на рынок», то есть крупная и мелкая заявка исполняются одинаково; комиссия фиксированная 0,05% вместо вашего тарифа; неисполненные заявки отменяются в конце сессии.

Прочитайте это ещё раз глазами трейдера. Проскальзывание, частичное исполнение, очередь в стакане и настоящие издержки в песочнице отсутствуют по построению. Ровно то, ради чего вы и хотели проверить стратегию «в реальных условиях».

Отсюда вывод, экономящий месяцы: бумажная торговля — это тест кода, а не тест стратегии. Она отвечает на вопрос «бот работает?», но не на вопрос «бот зарабатывает?». Ответ на второй вопрос даёт только живой счёт с минимальным размером позиции, и никакой другой инструмент его не заменит.

Есть и второй, более дешёвый способ получить то же самое — холостой режим внутри собственного кода: бот считает сигналы на живых данных и полностью формирует заявку, но в последний момент вместо отправки пишет её в лог. У нас это устроено так, что боевой режим включается переменной окружения, а не строчкой в исходнике. Это не мелочь: файл на рабочей машине и файл на сервере при таком подходе одинаковы побайтово, а торгует только тот экземпляр, которому среда разрешила. Прибить в локальной копии «торговлю выключить» — верный способ однажды залить эту копию на сервер и обнаружить, что боевой бот неделю вежливо простоял вхолостую.


Источник истины — брокер, а не память бота

Правило, которое мы формулировали ещё в первой статье серии: при старте бот обязан спросить у брокера реальную позицию и синхронизировать с ней своё состояние, а не наоборот. Это называется reconciliation (сверка, реконсиляция) — приведение внутреннего учёта в соответствие с учётом контрагента.

Правило верное, но недостаточное, и стоило нам отдельного инцидента.

Однажды на запрос портфеля пришёл ответ с кодом «200 OK» — то есть формально успешный, — но позиции по нашему инструменту в нём не оказалось. Позиция при этом была. Бот поверил ответу: снял с биржи живую заявку на закрытие, записал у себя выход, обнулил учёт. Через несколько минут пришёл очередной сигнал, и бот открыл позицию поверх уже существующей.

Реализованный убыток по этому эпизоду — ноль рублей. Именно поэтому его легко недооценить.

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

Разбор дал два вывода, оба переносятся на любого робота.

Первый: сверка, которая делается только при старте процесса, — не сверка. Условие «у брокера есть позиция, а бот её не открывал» проверялось ровно один раз за жизнь процесса. Поэтому ложное «я вне рынка» самоподдерживалось часами: бот считал себя плоским, а позиция жила без защитной заявки, без вечернего закрытия и без дневного лимита убытка — все три механизма были завязаны на признак «позиция есть».

Отсюда способ искать этот класс дефектов у себя. Возьмите любое состояние, которое бот держит в памяти, и задайте один вопрос: «если это состояние окажется неверным — кто и когда это заметит?» Ответ «никто» или «при следующем перезапуске» означает дыру.

Второй: «позиции нет» — это гипотеза, а не факт. Мы перестроили логику так, что исчезновение позиции признаётся только после серии согласованных ответов, растянутой во времени. Обоснование — через асимметрию последствий: поверить ложному нулю значит совершить необратимые действия (снять защиту, войти повторно); не поверить настоящему нулю значит записать выход на минуту позже, чем он состоялся. Первое стоит денег, второе не стоит ничего. Когда одна из двух ошибок бесплатна, выбор очевиден.


Третий ответ брокера: «не знаю»

Если бот отправил заявку и не получил ответа — истёк таймаут, оборвалось соединение, шлюз вернул ошибку, — единственный честный ответ на вопрос «заявка ушла на биржу?» звучит как «неизвестно». Не «скорее всего, не ушла» и не «наверное, ушла».

Это не философия, а рабочее состояние, в которое ваш бот будет попадать регулярно.

У нас это выглядело так. Отправка заявки на закрытие позиции умерла по таймауту. Заявка при этом благополучно ушла на биржу и висела там активной — а бот её потерял, потому что вместе с ответом потерялся и её идентификатор. Позиция закрылась другим способом, и на плоском счёте осталась висеть живая заявка на продажу. Провисела три с половиной часа. Исполнись она — и у нас образовалась бы позиция, о которой бот не знает ничего.

Частота такого события в нашем журнале — примерно один случай на полсотни отправок. Не «раз в жизнь», а несколько раз в квартал.

Разница между кодом, который об этом знает, и кодом, который не знает, — одна ветка else:

три исхода отправки вместо двух
resp = broker.place_order(...)        # запрос может не ответить вовсе

if resp.status == "OK":
    state.qty = resp.filled_qty       # факт: брокер подтвердил исполнение
elif resp.status == "REJECTED":
    state.qty = 0                     # факт: заявки на бирже нет
else:                                 # таймаут, обрыв, ошибка шлюза
    state.unknown = True              # ордер МОГ уйти. Повтора не будет
    alert("ответ не получен")         # дальше — опрос заявок и сделок

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

Что делать дальше — четыре приёма по возрастанию надёжности.

1. Не повторять отправку вслепую. Самая соблазнительная реакция на «ответ не пришёл» — послать ещё раз. Так у нас и было: до четырёх попыток подряд, что при неудачном стечении обстоятельств давало учетверённый объём.

Промежуточный вариант — повторять только после сверки: спросить позицию, и если она на месте, значит, ордер не прошёл, можно слать заново. Мы отказались и от него.

«Позиция на месте» доказывает не «ордер не прошёл», а «мы этого ещё не видим». Рыночная заявка исполняется за миллисекунды, а данные о позиции могут отставать — тем более в тот самый провал связи, из-за которого отправка и не ответила.

2. Спрашивать брокера о факте, а не выводить его. У API есть методы, отдающие список заявок и список сделок. Заявка со статусом и количеством исполненного — это факт. Ваша догадка «раз цена коснулась уровня, значит, заявка исполнилась» — не факт, а модель, которая ошибается ровно в самые нервные дни.

3. Пользоваться ключом идемпотентности. Idempotency (идемпотентность) — свойство операции давать один и тот же результат при повторном выполнении. В торговых API она реализуется через клиентский идентификатор заявки: вы сами придумываете уникальный ключ и отправляете его вместе с заявкой; повторный запрос с тем же ключом второй заявки не создаёт.

Здесь важно не поверить на слово, а проверить у своего брокера. У Т-Инвестиций это документировано явно: order_id в методе выставления заявки — ключ идемпотентности, при нескольких запросах с одинаковым ключом на биржу уйдёт только одно поручение, а повторные вернут ошибку 30057; уникальность отслеживается за последний месяц. У Финама поле client_order_id в заявке есть — необязательное, до 20 символов, генерируется автоматически, если его не передать, — но идемпотентности в описании метода не заявлено.

Разница принципиальная. Есть гарантия — пользуйтесь, это самый дешёвый способ закрыть весь класс проблемы целиком. Не заявлена — стройте защиту сами и не рассчитывайте, что «наверное, дубль не пройдёт».

4. Настойчивость выносить из момента наружу. Четыре попытки подряд укладываются в пятнадцать секунд — за это время сеть не успевает починиться, зато успевает накопиться очередь отправленных вами заявок. Правильнее одна попытка, а дальше повтор из главного цикла с нарастающей паузой: минута, три, десять. Заодно это не даёт упереться в лимиты API: у Т-Инвестиций, например, документированы 15 запросов в секунду на выставление заявки, 300 в минуту на отмену и не более 1000 запросов в минуту с одного IP-адреса. Цикл повторов без пауз выбирает такой лимит за секунды — и вы получаете отказ уже не от рынка, а от собственной настойчивости.


Четыре вопроса о каждом ордере

О каждой отправляемой заявке бот должен уметь ответить на четыре вопроса: откуда взялся объём, откуда взялось направление, сколько раз она будет отправлена и на чём основано «мы закрылись». Это те четыре вещи, которые в отправке вообще можно испортить, — наш робот прошёл серию аудитов, и задним числом видно, что каждый закрывал ровно один из этих вопросов.

Вопрос 1. Откуда взялся объём? Если количество контрактов пришло из ответа API, который может отстать, соврать или прийти пустым, — вы отправляете на биржу недоверенное число.

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

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

Вопрос 4. На чём основано «мы закрылись»? Бот записывает прибыль в отчёт. Откуда он знает, что сделка состоялась и по какой цене — из ответа брокера с количеством исполненного или из собственной эвристики «цена коснулась уровня, значит, исполнилось»?

Всё это сворачивается в один принцип, который мы теперь применяем к любому новому боту:

Недоверенное число никогда не задаёт объём и направление отправляемого ордера, а прибыль записывается только по доказанному факту.

Из него два практических следствия.

Асимметрия «недо-» и «пере-». Когда вы не уверены, закрылась позиция или нет, у ошибки два направления. Недозакрыть — оставить известную позицию, которую человек увидит в терминале и закроет руками. Перезакрыть — открыть новую позицию, о которой не знает никто и которую ничто не ограничивает. Эти ошибки не равноценны, и в сомнительной ситуации бот должен выбирать первую.

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


Данные отстают, и у отставания две стороны

Учёт брокера отстаёт от собственных сделок клиента: ответ «позиции нет» может прийти через секунду после того, как позиция открыта. Ровно это мы и увидели, разбирая ложный ноль, — и деталь оказалась важнее самого инцидента. Это не «API иногда врёт», а закономерное отставание учётной системы за только что исполненной сделкой.

Понимание меняет диагноз целиком. Если API врёт случайно — лечится повторным опросом. Если API систематически не успевает за вашим же действием, то опасно любое место, где бот спрашивает состояние сразу после того, как что-то сделал.

А дальше мысль, которую мы в первый раз пропустили и получили из-за этого второй инцидент, зеркальный первому.

У отставания две стороны. Если система не успевает показать открытие позиции, то ровно так же она не успевает показать её закрытие. Первую сторону мы увидели и защитились. Вторая осталась: сторож, следящий за «чужой» позицией, вставал в тревогу по одному ответу, в котором позиция ещё числилась живой, — хотя закрыта она была секундой раньше. Итог — сутки простоя и человек, ищущий призрак.

Тот же класс ошибки живёт и в данных для сигналов. Котировки, которые бот получает опросом, — это снимок на момент запроса, и он может быть на несколько секунд старше реальности. Бар, который по часам уже закрыт, в снимке может быть ещё не финальным: цена закрытия и объём успеют измениться. Мы на этом обожглись — решения принимались по неокончательным числам. Лечится тем, что признак «бар закрыт» считается не по системным часам, а по времени, на которое данные фактически получены.

Правило целиком: всякий ответ описывает прошлое. Вопрос лишь в том, насколько давнее — и достаточно ли этого для решения, которое вы собираетесь принять.


Сторожа: как они спасают и как стоят денег

Watchdog (сторож) — отдельный механизм, который следит не за рынком, а за самим роботом и его состоянием. Каждый сторож умеет не только вмешаться в торговлю, но и позвать человека — прислать уведомление, например на электронную почту. Минимальный набор для живого бота выглядит так:

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

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

У проверки, живущей в состоянии между вызовами, должен быть срок годности

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

Отсюда универсальный тест для любого сторожа, который что-то помнит между вызовами: спросите «а если между двумя ответами пройдёт час?». Ответ «ничего не изменится» — это дефект, а не устойчивость.

Сторож может стоить денег, и это надо считать отдельно

Молчаливый сторож, который при срабатывании просто перестаёт торговать, стоит вам недополученной прибыли за всё время, пока человек не заметил. У нас такой механизм не сбрасывается сам, только человеком, — и цена его ложного срабатывания измеряется в рублях простоя за сутки.

Это не аргумент против сторожей. Это аргумент за то, чтобы сторож не просто предупреждал, а отказывался от опасного действия, и чтобы у ложного срабатывания была понятная и не слишком высокая цена.

Сначала цена события, потом частота

Типичная ошибка разбора: «этот сторож срабатывает в четверти случаев, дефект регулярный, надо чинить». Мы дважды на ней поскользнулись, и оба раза вывод менялся на противоположный.

Первый случай: страховочное закрытие срабатывало часто, и это выглядело тревожно. Посчитали цену срабатывания — она статистически неотличима от нуля. Частота события, которое ничего не стоит, не аргумент вообще.

Второй тоньше. «26% выходов ушли в аварийную ветку» — цифра из отчёта. При разборе выяснилось, что часть случаев приходится на дни, которые бот уже не торгует: их отсёк фильтр, добавленный позже. Эти случаи физически не могут повториться, и в знаменателе им не место. Правило: частоту дефекта считают после фильтров, которые уже стоят в бою, а для каждого случая задают вопрос «а сегодня этот день вообще торгуется?».

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


Стоп, который переживёт вашего бота

Защитная заявка — единственное, что стоит между вами и открытой позицией в тот момент, когда бот умер. Поэтому вопрос «где физически живёт мой стоп» важнее, чем кажется, а ответов на него три.

1. В памяти бота. Бот следит за ценой и, когда она доходит до уровня, отправляет рыночную заявку. Самый простой вариант — и бесполезный именно в аварии: процесс умер, вместе с ним умер и стоп. Позиция остаётся на рынке без всякой защиты.

2. На стороне брокера. Классические условные заявки в QUIK устроены так: они хранятся на сервере брокера и на биржу не передаются, пока не наступит условие. Работают независимо от того, включён ли ваш терминал и жив ли ваш бот, — но с оговоркой, которую стоит знать: при блокировке или истечении прав доступа клиента к системе такие заявки не активируются. В торговых API это отдельные типы заявок: у Финама в описании метода выставления есть ORDER_TYPE_STOP и ORDER_TYPE_STOP_LIMIT плюс отдельный эндпоинт для пары стоп-лосс/тейк-профит, у Т-Инвестиций — отдельный сервис стоп-заявок.

3. В стакане на бирже. Обычная лимитная заявка на закрытие позиции. Она стоит в очереди и исполнится, даже если у вас сгорит и бот, и сервер, и интернет-провайдер. Для фиксации прибыли это лучший вариант: вы заранее знаете цену. Для стопа не подходит — лимитка не спасёт, если цена проскочит уровень разрывом.

В первой статье мы написали «стоп-заявка, физически размещённая на бирже». Формулировку стоит уточнить: важно не то, что заявка попала именно на биржу, а то, что она живёт вне процесса, который может умереть. Разумная схема для розничного бота — тейк лимитной заявкой в стакане, стоп условной заявкой на стороне брокера, а логика бота поверх этого только уточняет и переставляет уровни.

И маленькая ловушка, о которой мало кто думает заранее. Раз тейк живёт отдельно от бота, то при выходе из позиции другим способом эту заявку надо снять, а снятие — такая же операция с тремя исходами, как и постановка. Именно так у нас и получился тот самый живой «фантом» на плоском счёте. Каждая заявка, которую бот оставляет на бирже, — это обязательство её потом убрать; и код должен убеждаться, что убрал, а не считать, что убрал.


Где живёт бот: сервер, запуск, обновление

Домашний компьютер годится ровно до первого живого рубля. Дальше — арендованный сервер (VPS) за несколько сотен рублей в месяц: он не спит, не перезагружается на обновлениях Windows и не зависит от вашего роутера. Бот на нём оформляется системной службой, чтобы подниматься самостоятельно после перезагрузки и после падения.

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

Скопировать файл на сервер — не значит обновить бота. Программа читает свой исходник один раз, при запуске. Пока вы не перезапустили службу, на сервере работает старый код — и вы будете уверенно ждать поведения, которого там нет. Проверка после обновления состоит из трёх шагов: сверить контрольную сумму файла на сервере с локальной, перезапустить службу, убедиться по журналу, что стартовал именно новый код. Мы для этого печатаем при старте «шапку» с ключевыми параметрами: появились в журнале новые числа — код точно новый.

Не копируйте папку целиком. Рекурсивное копирование затрёт на сервере всё, что появилось там за время работы: файл состояния, журналы, накопленную историю сделок. Заливать — явным списком файлов кода.

Боевой режим — из окружения, а не из исходника. Мы говорили об этом выше, но повторим здесь, потому что это правило деплоя: признак «торгуем по-настоящему» задаётся переменной среды на сервере. Тогда файл везде одинаков, а вторая копия физически не может торговать.

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

Часовые пояса. Сервер живёт по UTC, биржа — по московскому времени, вы — по своему. Стандартный логгер пишет по времени машины, а ваш собственный код может писать по московскому: в двух файлах одного бота окажется разное время одного события. Отдельная ловушка при планировании: «обновлюсь в три ночи по-местному» для человека восточнее Москвы означает самый конец вечерней сессии. Всё, что касается торгов, считайте в биржевом времени и логи приводите к нему же.

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

Ротация логов должна уметь работать с открытым файлом. Бот держит журнал открытым, и стандартное «переименовать и создать новый» оставит вас без записей — нужен режим с обрезанием на месте.

И последнее, что относится уже не к серверу, а к рынку.

Расписание торгов — тоже входные данные, и они меняются. 23 марта 2026 года срочный рынок Московской биржи перешёл на единую торговую сессию: промежуточный клиринг отменён, основная клиринговая сессия перенесена на 23:50–00:30, вечерняя сессия теперь относится к текущему торговому дню, а не к следующему, торги идут непрерывно — утренняя сессия с 07:00, основная с 10:00 до 19:00, вечерняя до 23:50.

Для бота, у которого границы сессий и клиринговые перерывы зашиты числами, это означает, что в один прекрасный день он начал делать не то, что задумано, — и не сообщил об этом, потому что с его точки зрения ничего не произошло. Полновыборочный бэктест к такому слеп по построению: он усредняет годы старой микроструктуры с годами новой. Держите список того, что бот считает неизменным (расписание, размер лота, шаг цены, ставки обеспечения, даты экспирации), и проверяйте его при каждом изменении правил площадки. Заодно помните, что биржа поднимает ставки гарантийного обеспечения перед длинными праздниками и при расширении ценовых границ: размер позиции, который вчера помещался на счёт, завтра может не поместиться.


Как узнать, что бот сломался

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

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

Оповещения — только о том, что требует человека. У нас на электронную почту уходит один уровень сообщений — «нужно вмешаться»: не смог закрыть позицию, расхождение объёма с брокером, сработал дневной лимит, обнаружена чужая позиция. Входы, выходы и штатные предупреждения не отправляются никуда. Причина простая: канал, который пищит на каждую сделку, перестают читать — и он не сработает ровно в тот день, когда нужен.

Три технические детали, каждая появилась у нас после отдельных граблей:

  • Отправка не должна блокировать торговлю. Почтовый сервер отвечает секунды, а иногда не отвечает вовсе; если бот ждёт отправки в основном цикле, он пропускает рынок. Уведомления уходят в фоне.
  • Дедупликация — по тексту без чисел. Одна и та же авария, повторяющаяся каждые пять минут с растущей суммой убытка, при наивном сравнении выглядит как поток разных сообщений. Сравнивать надо «подпись» текста с вырезанными числами — включая пробелы-разделители разрядов, иначе «−1 234 ₽» и «−1 250 ₽» снова окажутся разными авариями.
  • Тип исключения печатать обязательно. «Не удалось отправить письмо» — бесполезная строка. «Ошибка аутентификации» и «таймаут соединения» — разные диагнозы: первый про пароль, второй про закрытый порт.

Молчание канала неоднозначно. «Всё хорошо» и «бот умер два дня назад» выглядят одинаково — тишиной. Поэтому ежедневная проверка живости руками из рутины не убирается: посмотреть, что журнал сегодня наполняется и что в нём нет строк уровня ошибки.

Самое коварное: молчаливый отказ

Теперь история, которая изменила наш подход к проверкам сильнее всего остального.

В боте была функция, разбирающая ответ брокера со списком заявок, — та самая, что должна подбирать «потерянную» после таймаута заявку. Работала она так: если нужной заявки в ответе нет, вернуть «не нашёл».

Проблема была в разборе. Брокер отдаёт запись о заявке двухэтажной: часть полей на верхнем уровне записи, часть — во вложенном объекте. Наш код спускался во вложенный и читал оттуда всё подряд. Поля с инструментом совпадали, а статус получался пустым — и срабатывала осторожная ветка «нет доказательства, что заявка жива, значит, не берём». Отсеивалась каждая заявка, всегда.

Функция была мертва одиннадцать дней. Мониторинг не показывал ничего, потому что показывать было нечего: молчаливый отказ разборщика неотличим от штатного отрицательного ответа. «Не нашёл заявку» — нормальный, предусмотренный ответ. То, что он стал единственно возможным, не видно ниоткуда.

Обнаружилось это только тогда, когда механизм понадобился по-настоящему — и не сработал.

Отсюда три правила, которые мы теперь применяем к любой проверке:

  1. Критерий успеха формулировать как «функция вернула заранее известное значение», а не «функция не вернула пустоту». Второе проходит и на сломанном коде.
  2. Печатать, какой веткой получен ответ. Иначе «не нашёл» и «не смог разобрать» сливаются в одно.
  3. Контроль чувствительности: скормить проверке заведомо испорченный вход. Если она отвечает одинаково на правильном и на сломанном — она не проверяет ничего. Мы наступили на это дважды, второй раз уже в роли аудитора: написали зонд, объявивший функцию живой, а он сам получал ответ по ветке «ничего не нашёл».

И родственное правило про приёмку: постоянно красная проверка равна выключенной. Если какой-то тест падает уже неделю по законной причине, его перестают читать — вместе со всеми, что стоят рядом.


Сверка боя с бэктестом

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

Правильный вопрос при сверке не «сколько заработали». Правильный — разложение: те же ли были сделки, по тем же ли ценам, с теми же ли издержками. Расхождение в каждой из трёх позиций означает свою болезнь: разные сделки — расхождение логики или данных; разные цены — недооценённое исполнение; разные издержки — неверный тариф в тесте.

Три грабли, на которые мы наступили при первой же сверке.

1. Боевой результат уже за вычетом комиссии, а тестовый — не обязательно. Если вы восстанавливаете размер сделки из журнала по сумме прибыли, комиссию нужно учесть, иначе получаются размеры позиций, которых бот физически не мог открыть, и вся сверка едет.

2. Посделочное сравнение обманывает из-за каскада. Первый прогон показал у бота «41 лишний вход». Артефакт: стоит боту и бэктесту разойтись в одном выходе, дальше расходится вся цепочка сделок дня — позиция освобождается в разное время, и следующие сигналы попадают в разные условия. Главное число при сверке всегда дневное; посделочное сравнение годится только как диагностика.

3. Окно сверки надо чистить от собственной истории правок. Ранний период боя непригоден: там ещё жили баги, починенные позже, и не было фильтров, добавленных позже. Сверять можно только тот отрезок, который идёт после последней правки поведения.

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

Результат нашей сверки мы уже назвали в начале: систематической операционной течи нет. Бэктест, сделанный по правилам из второй статьи серии, оказался честным, а весь минус месяца дал один сбой.


Что дороже: улучшить стратегию или не сломаться

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

Мы провели по своей стратегии больше двух десятков исследовательских раундов: пробовали фильтры, размеры позиции, время входа, форму выхода, дополнительные источники данных. Часть идей сработала, часть отвалилась — и ни одна не дотянула до той суммы, которую забрал единственный сбой.

Считается это просто, и считать надо именно в рублях:

что с чем сравнивать
     ожидаемая прибыль стратегии за месяц
                    против
цена одного отказа × сколько раз в год он случается

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

Почему это контринтуитивно и почему почти все делают наоборот: улучшение стратегии видно на графике, а устранённый отказ не виден нигде. Его цена — то, чего не случилось. Новая красивая кривая доходности приносит немедленное удовольствие, а неделя, потраченная на разбор веток «что если ответ не пришёл», не приносит ничего, кроме строчки в журнале изменений.

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

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


Чек-лист перед запуском торгового бота в бой

Первая статья заканчивалась минимальным чек-листом перед запуском. Этот — продолжение: то, что проверяется уже после того, как код написан, а бэктест честен.

Перед первым живым рублём

  • Бот отработал в бумажном режиме не меньше недели без падений, включая ночь и выходные.
  • Проверено, что бот правильно понимает расписание торгов, перерывы и календарь выходных площадки.
  • Боевой режим включается извне — переменной окружения, а не строчкой в коде.
  • Секреты лежат в файле с ограниченными правами; в коде их нет, и в кэшах тоже.
  • При старте бот запрашивает позицию у брокера и синхронизируется с ней.
  • Сверка позиции повторяется в течение дня, а не только при запуске.
  • Для каждой отправки известно, что делать при ответе «неизвестно»; повторная отправка вслепую запрещена.
  • Выяснено, поддерживает ли API вашего брокера идемпотентность заявки по клиентскому идентификатору. Если нет — защита от дублей написана своя.
  • Защитная заявка живёт вне процесса бота: у брокера или в стакане.
  • Есть дневной лимит убытка и потолок размера позиции.
  • Канал оповещений проверен отправкой тестового сообщения — включая папку «Спам».

Первый месяц

  • Минимальный размер позиции. Не «пока не разберусь», а фиксированный срок.
  • Ежедневный взгляд в журнал: сегодня записи есть, ошибок нет.
  • Каждое непонятное сообщение разбирается до конца в тот же день. Непонятое сегодня повторится в худший момент.
  • Обновления кода — вне торговой сессии, с сохранённой предыдущей версией и с проверкой, что запустился именно новый код.
  • Служба не останавливается с открытой позицией.
  • История сделок копится в виде, удобном для сверки: время, цены, объём, ответы брокера.

Через месяц-два

  • Сделана сверка боя с бэктестом на очищенном от правок окне, по дневным числам.
  • Посчитана цена каждого случившегося сбоя в рублях и его частота.
  • Проверено, что сторожа не просто стоят, а срабатывают: подсадите ошибку в копию бота и убедитесь, что защита её ловит.
  • Список того, что бот считает неизменным (расписание, лот, шаг цены, экспирации), проверен на актуальность.

Что дальше

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

Четвёртая статья серии об алготрейдинге — о том, где брать котировки и что с ними не так: чем API брокера отличается от биржевого источника, какая история реально доступна по российским инструментам и на какую глубину, почему склейка фьючерсных контрактов создаёт скачки, которых не было на счёте, как выглядят дыры и ошибочные тики в бесплатных данных и почему стратегию, проверенную на одном источнике, стоит перепроверить на другом.

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


Материал носит информационный характер и не является индивидуальной инвестиционной рекомендацией. Торговля на финансовых рынках связана с риском потери капитала.

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

© 2026 Tradeonia. Полное или частичное копирование материала, его переработка и публикация на других ресурсах без письменного разрешения правообладателя запрещены. Цитирование допускается в объёме, оправданном целью цитирования, с указанием автора и активной гиперссылкой на эту страницу.


Источники сведений, приведённых в статье

  • Правила работы песочницы (исполнение рыночных заявок по цене последней сделки, отсутствие влияния заявок на рынок, фиксированная комиссия 0,05%, отмена неисполненных заявок в конце сессии) — документация T-Invest API, раздел «Песочница».
  • Идемпотентность выставления заявки по ключу order_id, ошибка 30057 при повторе и срок отслеживания уникальности — документация T-Invest API, раздел о поручениях.
  • Лимиты на количество запросов (15 в секунду на выставление заявки, 300 в минуту на отмену, до 1000 запросов в минуту с одного IP-адреса) — документация T-Invest API, раздел «Лимиты».
  • Параметр client_order_id (необязательный, генерируется автоматически, до 20 символов), типы заявок ORDER_TYPE_STOP и ORDER_TYPE_STOP_LIMIT, отдельный эндпоинт стоп-лосс/тейк-профит — документация Finam Trade API.
  • Хранение условных заявок на сервере брокера, их независимость от состояния терминала и неактивация при блокировке прав доступа пользователя — руководство пользователя системы QUIK, раздел об условных заявках.
  • Переход срочного рынка Московской биржи на единую торговую сессию 23 марта 2026 года, отмена промежуточного клиринга, клиринговая сессия 23:50–00:30, отнесение вечерней сессии к текущему торговому дню и расписание сессий — материалы Московской биржи о единой торговой сессии срочного рынка.
  • Практика повышения ставок гарантийного обеспечения перед длинными праздниками и при расширении ценовых границ — материалы Московской биржи по риск-менеджменту срочного рынка.
  • Описания инцидентов, частот и приёмов разбора — журналы и результаты аудитов нашего боевого робота. Численные параметры стратегии, её инструмент и код не публикуются.

Частые вопросы

Чем реальная торговля робота отличается от бэктеста?
Главное отличие — третий исход у любого действия. В бэктесте заявка либо исполнилась, либо нет; в бою добавляется «неизвестно»: ответ не пришёл, и заявка могла как уйти на биржу, так и не уйти. Отсюда же второе отличие — позиций становится две: та, что в памяти бота, и та, что у брокера.
Заменяет ли бумажная торговля тест стратегии на реальном счёте?
Нет. Бумажная торговля — это тест кода, а не стратегии. В песочнице Т-Инвестиций рыночные заявки исполняются по цене последней сделки, заявки не влияют на рынок, а комиссия фиксированная 0,05% — то есть проскальзывания, очереди в стакане и настоящих издержек там нет по построению. Проверить доходность можно только живым счётом с минимальным объёмом.
Что делать, если брокер не ответил на отправку заявки?
Не отправлять её повторно вслепую. Сначала спросить у брокера факт — список заявок и список сделок, а не выводить его из движения цены. Само «позиция на месте» доказательством не служит: учёт брокера отстаёт от только что исполненной сделки на секунды. Повтор — из главного цикла с нарастающей паузой, а не четырьмя попытками подряд.
Что такое идемпотентность торговой заявки?
Это свойство операции давать один и тот же результат при повторе. В торговых API она делается клиентским идентификатором: вы придумываете ключ и шлёте его с заявкой. У Т-Инвестиций ключ идемпотентности — order_id: при нескольких запросах с одинаковым ключом на биржу уйдёт одно поручение, повторные вернут ошибку 30057, уникальность отслеживается за месяц.
Где должен находиться стоп-лосс торгового робота?
Вне процесса бота — условной заявкой на стороне брокера или заявкой в стакане. Стоп, который живёт в памяти программы и ждёт нужной цены, бесполезен ровно в аварии: процесс умер — умер и стоп, а позиция осталась на рынке без защиты. Обратная сторона: заявку, оставленную на бирже, придётся потом снять, и убедиться, что она действительно снята.
Нужен ли для торгового робота отдельный сервер?
Да, как только на счёте появляются реальные деньги. Аренда VPS стоит несколько сотен рублей в месяц, и он не спит, не перезагружается на обновлениях системы и не зависит от домашнего роутера. Бот на нём оформляется системной службой, чтобы подниматься сам после перезагрузки и после падения.
Что важнее для результата: улучшать стратегию или надёжность бота?
Считать надо в рублях: ожидаемую прибыль за месяц против цены одного отказа, умноженной на его частоту. У нас месячный результат целиком съел один сбой, а потолок любой отдельной идеи по улучшению стратегии оказался в разы меньше этой суммы. Пока нет месяцев живой работы, задача не поднять доходность, а дожить до статистики.
Материалы сайта носят информационный характер и не являются индивидуальной инвестиционной рекомендацией. Статьи отражают опыт и мнение редакции; примеры кода публикуются в учебных целях, предоставляются «как есть» и не гарантируют какого-либо результата. Торговля на бирже связана с риском потери вложенных средств.
© 2026 Tradeonia. Все права защищены.