Приветствую, коллеги-трейдеры и разработчики. Сегодня я хочу разобрать одну досадную, но поучительную историю из жизни моего торгового робота Scatter&Gather. Речь пойдет о версии v10.4_medium, в которой затаился коварный баг. Причем я хочу не просто сообщить о фиксе, а провести полноценный разбор полетов. Ведь каждая ошибка — это не провал, а бесплатный урок. И этот урок был о том, как тонкое архитектурное изменение может привести к неожиданным последствиям, если не учесть все нюансы управления состоянием системы.
Архитектурный сдвиг: От лимиток к виртуальным заявкам
Когда я задумывал переход от классических лимитных заявок к системе предзаявок (виртуальных заявок), я руководствовался желанием повысить гибкость. Раньше робот напрямую выставлял лимитные приказы в стакан. Это надежно, но несколько грубовато. В новой схеме робот сначала создает предзаявку — `pending_orders` — виртуальный план действий в собственной памяти. И только когда рынок достигает нужной цены, этот план материализуется в моментальную рыночную заявку с реальным `trans_id` в QUIK.
Почему я считаю это правильным? Это давало полное исполнение заявки, исключало «проскальзывание» и позволяло работать с более сложной логикой входа. Представьте разведчика, который не лезет на передовую, а сначала ставит метку на карте. Когда цель в зоне досягаемости, он мгновенно вызывает авиаудар. Элегантно, правда? Но я упустил один критически важный аспект: что происходит с этим «авиаударом», если в самый неподходящий момент разведчика вдруг срочно отзывают на переподготовку?
Именно это и случилось. В моей архитектуре есть подсистема `tracked_orders`, которая, как верный пес, следит за исполнением уже отправленных в QUIK рыночных заявок. И есть функция `StopTrade()`, которую я еще в более ранних версиях запрограммировал на полную «зачистку» перед обучением (тестированием на исторических данных) шага сделки в %.
Момент катастрофы: Когда обучение системы стирает её память
Давайте смоделируем роковую последовательность событий на примере с «Озон Фармацевтика». Робот стартовал на дивидендном гэпе — ситуация динамичная, нервная. Сработала предзаявка, мгновенно превратилась в рыночный приказ на покупку и улетела в биржу. И в этот самый миллисекундный промежуток между «отправил» и «исполнился» срабатывает триггер автообучения шага сделки.
Что делает робот? Он честно выполняет протокол: «Стоп торговля! Начинаем перекалибровку!». Функция `StopTrade()` принимается за работу. Она исправно очищает очередь виртуальных предзаявок (`pending_orders`) — зачем нам новые планы, если мы на паузе? Логично. Но затем она, следуя устаревшему шаблону, также обнуляет список `tracked_orders`. Вот он — фатальный сбой. Робот не просто останавливает торговлю, он стирает из оперативной памяти факт существования всех заявок. При лимитных заявках все работало верно — заявки снимаются и отслеживать нечего. А в системе предзаявок выставленная заявка не снимается, у нее только один путь — исполнение. Вопрос лишь в цене, по которой заявка будет исполнена.
А что в это время делает биржа? Она-то ничего не знает о наших внутренних обучениях. Заявка уже в стакане, и она благополучно исполняется. Но робот, закончив обучение, стартует торговлю заново с новым шагом сделки. Он чист, как белый лист. Он понятия не имеет, что у него уже есть открытая позиция по этой бумаге. Это все равно что отправить курьера с важной посылкой, а потом, переехав в новый офис, забыть о его существовании. Курьер-то придет, но к пустому помещению.
Как не странно это звучит, но подобная ошибка за месяцы тестирования не проявила себя ни разу (за исключением последнего случая с «Озон Фармацевтика».
Философия исправления: Разделение ответственности в коде
Осознав корень проблемы, я взялся за ее исправление. Ошибка была концептуальной: функция `StopTrade()` выполняла две разные работы под одним названием.
1. Управление активностью (остановка генерации новых сигналов).
2. Управление памятью (очистка данных о текущих процессах).
Их нужно было жестко разделить.
В версии v10.5_medium реализовано следующее правильное поведение:
- `pending_orders` очищаются при остановке. Это правильно. Новых предзаявок в режиме обучения быть не должно. Сносим карту, по которой разведчик больше не работает.
- `tracked_orders` НЕ ТРОГАЕМ НИКОГДА. Эта подсистема становится независимой. Её задача — мониторить всё, что уже ушло в биржу, до логического завершения (исполнения или отмены самой биржей). Она должна быть слепой к тому, находится ли основная стратегия в режиме торговли или обучения.
- Блокировка новых покупок и продаж во время обучения. Пока идет перекалибровка, система не имеет права инициировать новые сделки. Это страховочный пояс.
- Аккуратная уборка. Очистка `tracked_orders` происходит теперь только после того, как заявка в нем перешла в статус «Исполнена». Убрали со стола только ту тарелку, которая уже пуста.
Заключение
Этот инцидент стал для меня ярким напоминанием: любое изменение в архитектуре требует тотального регрессионного тестирования. Нельзя менять фундамент, не проверив, не треснули ли стены. Особенно в алготрейдинге, где код оперирует реальными деньгами.
Предлагаю скачать исправленную версию Scatter&Gather v10.5_medium. Пусть ваша торговля будет не только прибыльной, но и, в первую очередь, предсказуемой и контролируемой. Удачи на рынках.
Скачать робота:

