Блог им. DedBoroded

Торговый робот для Т-Инвестиций. Как обычно самая сложная часть — не стратегия

GUI bot

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

Недавно я завершил торгового робота под T-Invest API. И этот проект в очередной раз подтвердил старую мысль: придумать условие покупки и продажи обычно намного проще, чем сделать систему, которой не страшно дать доступ к реальному счёту.

На уровне первого прототипа всё выглядело довольно бодро:

получили цены > проверили условие > нашли сигнал > отправили заявку

Если задача — показать демку на своём компьютере, этого почти достаточно. Но как только появляются реальные деньги, начинается всякая мелкая, противная и очень важная работа. Заявка может исполниться частично. API может не ответить. Клиент может вручную что-нибудь купить или продать. Цена может оказаться старой. Программа может закрыться ровно в тот момент, когда брокер уже принял заявку, но ответ до робота ещё не дошёл. И вот тут простой скрипт заканчивается.

Заявку отправили. Дальше что?

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

  • брокер отклонил заявку;
  • заявка принята, но пока не исполнена;
  • исполнилась частично;
  • исполнилась полностью;
  • брокер заявку принял, но ответ потерялся из-за сетевого сбоя.

Поэтому перед отправкой заявки я сначала создаю её локально. У неё есть собственный order_request_id и первоначальный статус: PREPARED
После ответа брокера состояние меняется: PREPARED > SENT | FAILED | CANCELLED
Дополнительно сохраняются:

  • broker_order_id
  • запрошенное количество лотов
  • исполненное количество лотов
  • цена исполнения
  • общая сумма
  • статус брокера
  • текст ошибки

Локальную позицию робот изменяет только на реально исполненное количество. Запросили десять лотов, исполнилось четыре — значит, в позиции четыре. Казалось бы, очевидная вещь. Но я видел достаточно торгового кода, в котором позиция менялась сразу после вызова buy(). Пока всё работает идеально, разницы не видно. При первом частичном исполнении начинается бардак.

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

Робот не должен торговать всей позицией клиента

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

  • robot_lots     — сколько купил сам робот
  • broker_lots    — сколько реально лежит у брокера
  • external_lots  — сколько появилось помимо робота

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

Последняя цена не всегда последняя

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

Лучше пропустить сделку, чем открыть её по неизвестно какой цене.

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

Тестовый режим — это не просто отключённая кнопка покупки

Сначала я тоже рассматривал простой вариант: LIVE_TRADING = False

Но пользы от такого режима немного. Он говорит только о том, что сделка не была совершена. А что робот собирался сделать? Какой объём рассчитал? Почему пропустил сигнал? Хватало ли лимита? Влезал ли хотя бы один лот? Поэтому в тестовом режиме создаётся полноценный план операции. Для каждого сигнала сохраняется:

  • инструмент
  • текущая цена
  • стоимость лота
  • рассчитанное количество лотов
  • предполагаемая сумма
  • статус решения
  • причина отказа

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

Лимит денег — тоже не одна проверка

Перед покупкой система проверяет не только остаток рублей на счёте. Есть несколько разных ограничений:

  • сколько денег вообще разрешено использовать роботу;
  • сколько можно потратить на одну покупку;
  • сколько капитала уже занято позициями робота;
  • хватает ли свободных денег у брокера;
  • помещается ли хотя бы один лот;
  • нет ли уже позиции по этому инструменту;
  • не закрыл ли робот эту же позицию несколькими секундами ранее.

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

Почему сделал обычное Windows-приложение

Робот написан на Python. Интерфейс — PySide6, обмен с брокером — gRPC, локальное хранение — SQLite. Сетевые запросы и постоянный мониторинг выполняются не в основном потоке интерфейса. Иначе окно будет периодически зависать, особенно когда API отвечает медленно. Все результаты возвращаются в GUI через сигналы Qt. В приложении можно посмотреть:

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

Есть и ручное управление: выставить заявку, отменить активную, закрыть позиции робота, временно запретить покупки или продажи. Это не попытка написать новый QUIK. Просто если система работает с деньгами, у пользователя должна быть возможность понять, что она делает, и остановить её без поиска нужной строки в коде. Настройки, история заявок, сигналы и позиции хранятся в SQLite. Для одного локального приложения городить PostgreSQL с отдельным сервером смысла не было. Для клиента проект собирается в обычный .exe через PyInstaller. Python отдельно устанавливать не нужно. Папка с исходниками, виртуальное окружение и инструкция «откройте консоль, введите пять команд» — это не готовый продукт. Это заготовка для программиста.

Что в итоге

Торговая стратегия — это только причина что-то сделать. Сам торговый робот начинается дальше: когда нужно понять, можно ли совершать операцию, что реально исполнилось, какая позиция принадлежит роботу, как восстановиться после сбоя и как объяснить пользователю, что сейчас происходит. Разработчик торговой системы отвечает не за обещания доходности. Он отвечает за то, чтобы заложенная логика исполнялась предсказуемо и не превращала технический сбой в финансовый. Если кто-то сейчас делает робота под T-Invest API — задавайте вопросы. Интересно сравнить, как другие решают синхронизацию позиций и неопределённый статус заявок.

P. S. Если тема зайдёт, следующим могу написать про интеграцию с Interactive Brokers. Вот там нюансов уже столько, что одной статьёй отделаться будет трудно.

Данная публикация является личным мнением автора. Мнение владельца сайта может не совпадать с мнением автора.
383
2 комментария
Самое сложное — это токен.
avatar
Отличный нейрослоп. Развитие ИИ идет в нужное направление.

Статья конечно зайдет. 1.5 калеки прочитают здесь в пустом разделе.

Читайте на SMART-LAB:
Курс евро умеренно негативно отреагировал на решение ЕЦБ по процентным ставкам
Европейский центробанк 23 июля не стал рисковать и принял ожидаемое решение сохранить все ключевые процентные ставки без изменений. Так, базовая...
Фото
Итоги первичных размещений ВДО и некоторых розничных выпусков на 24 июля 2026 г.
Следите за нашими новостями в удобном формате: Telegram , Youtube , RuTube, Smart-lab , ВКонтакте , Сайт
⚡️⚡️⚡️ ЦБ снизил ставку до 14%
Экономика в целом во 2 квартале росла умеренными темпами. Существенный рост цен и повышение инфляционных ожиданий в летние месяцы во многом были...
Фото
РУСАГРО: возвращение дивидендов и неизбежных иксов
РУСАГРО помимо хорошего операционно отчета, выпустила долгожданный (хотя и невероятный) сущфакт «С учетом установленных Определением...

теги блога oreshkinalexey

....все тэги



UPDONW
Новый дизайн