Чем бой отличается от бэктеста
В бэктесте вход в позицию — это строка вроде 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. Посделочное сравнение обманывает из-за каскада. Первый прогон показал у бота «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, отнесение вечерней сессии к текущему торговому дню и расписание сессий — материалы Московской биржи о единой торговой сессии срочного рынка.
- Практика повышения ставок гарантийного обеспечения перед длинными праздниками и при расширении ценовых границ — материалы Московской биржи по риск-менеджменту срочного рынка.
- Описания инцидентов, частот и приёмов разбора — журналы и результаты аудитов нашего боевого робота. Численные параметры стратегии, её инструмент и код не публикуются.
