Блог им. DedBoroded

Сразу оговорюсь: саму торговую стратегию в статье показывать не буду. Не потому, что там спрятан очередной «грааль», который превращает тысячу рублей в квартиру на Патриках. Просто статья вообще не об этом.
Недавно я завершил торгового робота под T-Invest API. И этот проект в очередной раз подтвердил старую мысль: придумать условие покупки и продажи обычно намного проще, чем сделать систему, которой не страшно дать доступ к реальному счёту.
На уровне первого прототипа всё выглядело довольно бодро:
получили цены > проверили условие > нашли сигнал > отправили заявкуОдна из самых опасных ошибок — считать, что успешный вызов метода API означает совершённую сделку. Допустим, робот отправил заявку на покупку десяти лотов. Вариантов дальше несколько:
Поэтому перед отправкой заявки я сначала создаю её локально. У неё есть собственный order_request_id и первоначальный статус: PREPARED
После ответа брокера состояние меняется: PREPARED > SENT | FAILED | CANCELLED
Дополнительно сохраняются:
Локальную позицию робот изменяет только на реально исполненное количество. Запросили десять лотов, исполнилось четыре — значит, в позиции четыре. Казалось бы, очевидная вещь. Но я видел достаточно торгового кода, в котором позиция менялась сразу после вызова buy(). Пока всё работает идеально, разницы не видно. При первом частичном исполнении начинается бардак.
Самый неприятный случай — неопределённый результат. Брокер заявку получил, а робот не получил ответ. Повторить запрос вслепую нельзя: можно купить второй такой же объём. Просто считать заявку неисполненной тоже нельзя. В нормальной версии здесь нужен отдельный механизм сверки: после сомнительного результата робот запрашивает состояние заявки и только потом решает, что делать дальше. Это один из пунктов, который я ещё буду усиливать.
Робот не должен торговать всей позицией клиента
С этим моментом всё ещё веселее. Представим, что робот купил пять лотов Сбера. Потом владелец счёта вручную купил ещё три лота через приложение брокера. У брокера теперь восемь лотов. Но роботу принадлежат только пять. Если хранить только общую позицию брокера, робот решит, что все восемь лотов открыл он. При выходе из сделки он их благополучно продаст. Клиент, скорее всего, будет слегка удивлён. Поэтому я разделил три величины:
Перед запуском система запрашивает портфель и сверяет его с локальной базой. Если у брокера меньше лотов, чем числится за роботом, локальная позиция уменьшается. Например, пользователь вручную продал часть бумаг. Если у брокера больше, разница считается внешней позицией клиента. Робот её видит, но своей не считает и автоматически не трогает. Перед окончательным запуском эти позиции выводятся пользователю в интерфейсе. Их можно проверить и, при необходимости, скорректировать. На мой взгляд, это важная часть проекта. Торговая стратегия может быть какой угодно умной, но если робот не понимает, какие бумаги действительно принадлежат ему, до добра это не доведёт.
Последняя цена не всегда последняя
Следующая засада — рыночные данные. API вернул цену. Прекрасно. Только когда эта цена была сформирована? Инструмент мог перестать торговаться. Данные могли задержаться. Могла вернуться последняя известная сделка, которой уже несколько минут. Поэтому вместе с ценой робот проверяет её время. Если данные старше установленного ограничения, инструмент пропускается, а причина записывается в журнал. Логика здесь простая:
Лучше пропустить сделку, чем открыть её по неизвестно какой цене.
Цены по рабочему списку получаются пачками. Делать отдельный запрос на каждую акцию — лишняя нагрузка и лишнее время. Если проблема возникла только с одним инструментом, весь цикл не падает. Проблемная акция уходит в список пропущенных, остальные продолжают обрабатываться.
Тестовый режим — это не просто отключённая кнопка покупки
Но пользы от такого режима немного. Он говорит только о том, что сделка не была совершена. А что робот собирался сделать? Какой объём рассчитал? Почему пропустил сигнал? Хватало ли лимита? Влезал ли хотя бы один лот? Поэтому в тестовом режиме создаётся полноценный план операции. Для каждого сигнала сохраняется:
Реальной заявки при этом нет. Можно запустить робота, дать ему поработать и потом спокойно посмотреть, какие действия он собирался совершать. Не просто набор сигналов, а именно решения с учётом денег, лотов и ограничений. Боевой режим по умолчанию выключен. После перезапуска он сам не включается. Перед запуском пользователь дополнительно подтверждает, что понимает: сейчас пойдут реальные заявки. Покупки и продажи можно отключать отдельно. Например, запретить новые входы, но оставить выход из уже открытых позиций.
Лимит денег — тоже не одна проверка
Перед покупкой система проверяет не только остаток рублей на счёте. Есть несколько разных ограничений:
Последний пункт нужен, чтобы не получить идиотскую ситуацию: робот в одном цикле вышел из позиции, а потом по новому сигналу тут же открыл её обратно. Пирамидинг в текущей версии тоже запрещён. Если позиция уже есть, повторная покупка пропускается. Можно спорить о самой торговой логике, но слой ограничений должен быть отдельным. Сигнал ещё не означает, что сделку разрешено совершать.
Почему сделал обычное Windows-приложение
Робот написан на Python. Интерфейс — PySide6, обмен с брокером — gRPC, локальное хранение — SQLite. Сетевые запросы и постоянный мониторинг выполняются не в основном потоке интерфейса. Иначе окно будет периодически зависать, особенно когда API отвечает медленно. Все результаты возвращаются в GUI через сигналы Qt. В приложении можно посмотреть:
Есть и ручное управление: выставить заявку, отменить активную, закрыть позиции робота, временно запретить покупки или продажи. Это не попытка написать новый QUIK. Просто если система работает с деньгами, у пользователя должна быть возможность понять, что она делает, и остановить её без поиска нужной строки в коде. Настройки, история заявок, сигналы и позиции хранятся в SQLite. Для одного локального приложения городить PostgreSQL с отдельным сервером смысла не было. Для клиента проект собирается в обычный .exe через PyInstaller. Python отдельно устанавливать не нужно. Папка с исходниками, виртуальное окружение и инструкция «откройте консоль, введите пять команд» — это не готовый продукт. Это заготовка для программиста.
Торговая стратегия — это только причина что-то сделать. Сам торговый робот начинается дальше: когда нужно понять, можно ли совершать операцию, что реально исполнилось, какая позиция принадлежит роботу, как восстановиться после сбоя и как объяснить пользователю, что сейчас происходит. Разработчик торговой системы отвечает не за обещания доходности. Он отвечает за то, чтобы заложенная логика исполнялась предсказуемо и не превращала технический сбой в финансовый. Если кто-то сейчас делает робота под T-Invest API — задавайте вопросы. Интересно сравнить, как другие решают синхронизацию позиций и неопределённый статус заявок.
P. S. Если тема зайдёт, следующим могу написать про интеграцию с Interactive Brokers. Вот там нюансов уже столько, что одной статьёй отделаться будет трудно.
Статья конечно зайдет. 1.5 калеки прочитают здесь в пустом разделе.