
Материал посвящён разработке программного обеспечения. Саму стратегию и результаты её работы я не раскрываю, инвестиционных рекомендаций здесь тоже нет.
Если стратегия теряет деньги, её не спасут идеальная архитектура, модный стек и сотни автоматических тестов. Можно завернуть убыточный алгоритм хоть в Kubernetes — прибыльным он от этого не станет.
Но верно и обратное. Нормальную торговую идею вполне реально угробить плохой реализацией. Робот пропустил исполнение заявки, перепутал свою позицию с ручной, повторно отправил ордер после сетевой ошибки — и стратегия уже торгует совсем не так, как была задумана.
Недавно я закончил разработку торгового робота для работы через T-Invest API. В этой статье покажу чем боевой торговый продукт отличается от скрипта, который научился отправлять заявки брокеру.
Для меня граница довольно простая.
Если робот работает только тогда, когда пользователь сидит рядом, смотрит в лог и готов в любой момент вмешаться, это ещё не автономная торговля. Это полуавтоматическая система с дополнительным источником стресса.
Боевой продукт начинается тогда, когда можно уйти на встречу или уехать в отпуск и не проверять каждые пять минут, что робот там натворил.

Первый прототип почти всегда выглядит прилично
На базовом уровне любой торговый робот устроен элементарно:
получить рыночные данные
проверить торговое условие
сформировать сигнал
отправить заявку брокеру
Такой прототип можно написать довольно быстро. И он даже будет торговать. Проблема в том, что реальный рынок не обязан вести себя так аккуратно, как код из учебного примера.
Заявка может исполниться частично. API может не ответить. Ответ может потеряться. Пользователь может вручную изменить портфель. Цена может оказаться старой. Приложение может закрыться в тот момент, когда брокер уже принял поручение, но локальное состояние ещё не обновилось.
Это не набор фантастических сценариев, придуманных для усложнения технического задания. Это обычная жизнь системы, которая долго работает с внешним API и реальными деньгами.
Заявку отправили — это ещё не сделка
Допустим, робот отправил брокеру заявку на покупку десяти лотов. Дальше её могут отклонить, принять без исполнения, исполнить частично или выполнить полностью. Возможен и более неприятный вариант: брокер заявку получил, но приложение не дождалось ответа.
Поэтому сначала заявка создаётся внутри самого робота. Она получает собственный идентификатор и локальный статус PREPARED. Только после этого запрос отправляется брокеру. Упрощённо техническое состояние выглядит так:
PREPARED → SENT / FAILED / CANCELLED
При этом SENT не означает, что заявка полностью исполнена. Отдельно сохраняются брокерский статус и фактически исполненный объём.
В базе остаются идентификатор заявки у брокера, запрошенное количество лотов, цена исполнения, итоговая сумма и текст ошибки, если она возникла.
Позиция робота меняется только после подтверждённого исполнения. Запросили десять лотов, исполнилось четыре — значит, позиция увеличилась на четыре. Не на десять и не на предполагаемый объём. Звучит банально, но торговый код нередко пишут примерно так:
await buy() position += quantity
Работать это будет ровно до первого нормального сбоя. Самая неприятная ситуация возникает, когда брокер заявку принял, а робот получил сетевую ошибку. Повторять запрос вслепую нельзя — можно удвоить позицию. Считать, что сделки не было, тоже нельзя. Здесь нужна сверка состояния с брокером, а не вера в собственный код.
Робот не имеет права считать весь портфель своим
На одном брокерском счёте может одновременно работать робот и торговать сам пользователь. Например, робот купил пять лотов. Через час владелец счёта самостоятельно докупил ещё три. У брокера теперь восемь лотов, но робот открыл только пять. Если программа просто получает общий размер позиции и считает его своим, то при выходе она может продать все восемь лотов.
Для меня это не мелкая неточность. Это прямой косяк реализации: робот полез распоряжаться бумагами, которые ему не принадлежат. Поэтому внутри приложения отдельно учитываются три величины:
robot_lots = 6 # сколько купил сам робот broker_lots = 10 # сколько фактически находится у брокера external_lots = 4 # — сколько появилось помимо робота
Перед запуском система синхронизируется с реальным портфелем. Если пользователь вручную продал часть позиции, количество лотов робота уменьшается. Если у брокера бумаг больше, чем числится за роботом, разница считается внешней позицией. Робот её видит, но автоматически не трогает. Брокер остаётся источником фактического состояния. Однако знать только общий объём мало — нужно понимать происхождение позиции.
Перезапуск не должен обнулять память робота
Ещё одна граница между скриптом и боевой системой — поведение после перезапуска.
Робот не может утром загрузиться с пустыми переменными и решить, что заявок и позиций у него нет. Перед продолжением работы он должен получить портфель брокера, загрузить собственное состояние из базы и сверить одно с другим. Если локально числится позиция, которой у брокера уже нет, она обнуляется. Если реальный объём меньше — позиция робота уменьшается. Если больше — разница считается внешней.
С заявками ситуация сложнее.
Если перед закрытием приложения запрос был подготовлен или отправлен, но окончательный результат не успел сохраниться, такую операцию нельзя автоматически считать ни исполненной, ни отменённой. Сначала нужно запросить её состояние у брокера.
Запуск робота без предварительной синхронизации — это, по сути, торговля с потерей памяти. На тестовом счёте можно поэкспериментировать. На реальном — нет.
Последняя цена иногда уже никому не нужна
API вернул цену — отлично. Только когда по этой цене прошла последняя сделка?
Значение может быть технически правильным, но уже устаревшим. Инструмент перестал торговаться, данные задержались, наступил перерыв — вариантов хватает. Поэтому робот проверяет не только цену, но и время её формирования. Если данные старше установленного ограничения, инструмент пропускается.
Я здесь придерживаюсь простой позиции: пропущенная сделка лучше сделки, открытой по протухшим данным.
Цены по рабочему списку запрашиваются пачками. Если данные не пришли по одному инструменту, весь торговый цикл не падает. Конкретная акция пропускается, причина записывается в журнал, а остальные продолжают обрабатываться. Останавливать всю систему из-за одного проблемного инструмента — слишком хрупкое решение.
Тестовый режим должен проверять торговлю, а не наличие кнопки
Можно добавить флаг:
LIVE_TRADING = False
При выключенном флаге заявки не отправляются. Формально тестовый режим существует. Практической пользы от него почти нет.
Нужно видеть не только сигнал, но и решение робота целиком: какой инструмент он выбрал, сколько лотов рассчитал, сколько денег потребовалось и почему операция была разрешена или пропущена. Поэтому в тестовом режиме приложение сохраняет полноценные планы сделок. Рыночные данные реальные, расчёты реальные, ограничения тоже реальные. Не отправляется только заявка брокеру.
Так можно посмотреть, как система действительно собиралась торговать, а не просто убедиться, что внутри программы иногда появляется слово BUY.
Боевой режим включается отдельно и требует подтверждения. Покупки и продажи можно разрешать независимо друг от друга. Например, можно запретить новые входы, но оставить автоматическое закрытие уже открытых позиций.
Сигнал не даёт роботу право немедленно покупать
Стратегия сформировала сигнал. Дальше система обязана проверить, можно ли эту сделку вообще совершать. В моём случае учитываются свободные деньги, общий лимит капитала робота, размер одной операции, стоимость лота, наличие уже открытой позиции и доступность инструмента для торговли через API.
Если пирамидинг не предусмотрен стратегией, вторую позицию по тому же инструменту робот не открывает.
Отдельно блокируется повторный вход сразу после выхода в том же цикле. Иначе можно получить довольно глупую ситуацию: робот закрыл позицию и через несколько секунд снова открыл её по всё ещё активному сигналу.
Торговая логика говорит, что хочется сделать.
Слой исполнения решает, разрешено ли это делать сейчас.
Смешивать эти две части в одну кашу — плохая идея. Стратегию потом трудно нормально тестировать, а ограничения начинают незаметно влиять на формирование сигналов.
Почему я сделал отдельное приложение
Робот написан на Python. Графический интерфейс сделан на PySide6, обмен с брокером идёт через gRPC, локальное состояние хранится в SQLite.
Я не считаю, что каждому торговому роботу обязательно нужен GUI. Серверная система вполне может годами работать без единого окна. Но если приложение передаётся пользователю, интерфейс нужен. Человек должен видеть, что происходит с его деньгами, не разбирая логи в терминале.
В рабочем окне отображаются счёт и свободные средства, выбранные инструменты, сигналы, планы операций, заявки, позиции и история торговых циклов. Ошибки вынесены в отдельный журнал.
Сетевые запросы и постоянный мониторинг выполняются не в основном потоке, поэтому окно не зависает во время обращения к API.
Есть и ручное управление: можно выставить или отменить заявку, закрыть позиции робота, временно запретить покупки либо продажи.
Кнопка аварийного вмешательства не делает стратегию прибыльнее. Но когда робот работает с крупным счётом без присмотра, она резко перестаёт казаться лишней.
Готовое приложение собирается под Windows. Пользователь получает запускаемый продукт, а не папку с Python-файлами и инструкцию по созданию виртуального окружения.
А что насчёт отпуска?
В начале я написал, что боевого робота можно оставить работать не только на время встречи, но и уехать в отпуск. Здесь нужно разделять две задачи. Одно дело — автономная работа внутри торговой сессии. Робот запущен, соединение с брокером есть, база доступна, а пользователь несколько часов не смотрит на экран.
Совсем другое — оставить систему без присмотра на несколько дней или недель. Для этого уже недостаточно корректной торговой логики и учёта состояний. Нужны внешний мониторинг процесса, автоматический перезапуск, контроль доступности, уведомления о критических ошибках, резервирование данных и возможность удалённо остановить торговлю.
Разбирать этот уровень эксплуатации здесь я не стал, иначе статья разрослась бы ещё в два раза. Но без него фраза «робот может работать, пока владелец в отпуске» будет скорее красивым обещанием, чем нормальной инженерной гарантией.
Интересно, где эту границу проводят другие разработчики и трейдеры. Какие сбои или расхождения состояний оказались самыми неприятными в ваших системах?
Комментарии (8)

VsBirdEye
05.08.2026 12:00По опыту запиливания похожей системы для крипторынка, всё перечисленное — стейт-машина заявок, чужие позиции, сверка после рестарта, протухшие данные — подтверждаю как обязательный фундамент.
Про «самую неприятную ситуацию» (брокер принял, ответ потерялся). Есть вторая гонка того же класса: отмена против исполнения. Отмена не считается успешной, пока не подтверждена, и если в окне гонки пришло исполнение — побеждает исполнение. Без явной семантики «fill wins» легко получить «отменённую» позицию, которая на самом деле открылась.
Поделюсь тем, где началась следующая граница надежности:
1. Главный сдвиг мышления: перечислять не отказы, а допущения. Любой список сценариев («API не ответил», «частичное исполнение») неполон — следующий сбой будет не из списка. Вместо этого перечисляем допущения, на которых стоит корректность системы: «наблюдаемая цена отражает реальность», «ордер размещается за ограниченное время», «цена исполнения близка к наблюдаемой», «внутреннее состояние равно брокерскому», «единица учёта стабильна» — и на каждое нужен монитор. Будущее tail-событие проявится как нарушение их подмножества.
2. Отказы частичны. Криптокрах 10 октября 2025 показал: отмена работала, размещение — нет, данные шли, но глубина стакана испарилась на 98%. «API доступен/недоступен» — слишком грубая модель; полезно вести здоровье по каналам (размещение / отмена / маркет-дата / аккаунт) раздельно.
3. Охранять выход, а не убыток. Стоп по просадке смотрит назад — и в фазе испарившейся ликвидности стреляет в пустой стакан. Форвардная метрика «закрывается ли портфель прямо сейчас в пределах X% слиппеджа» велит сокращаться, пока стакан еще жив.
4. Для проектирования защитного слоя полезно задавать архитектурный вопрос - «какая общая причина отключит и слой, и то, что он защищает?». Отсюда рождаются решения вида сторожевой контур на отдельном хосте/маршруте/ключе, который умеет только сокращать и отменять и срабатывает при «робот мёртв ∧ позиции открыты ∧ биржа жива».
5. Биржевые SL/TP как страховка не работают — взводить их когда канал размещения уже деградировал бессмысленно, а постоянный взвод заранее показывает рынку вашу точку боли. Отсутствие гарантий их выполнения - постоянный источник грустных мемов)

OreshkinAlexey Автор
05.08.2026 12:00Спасибо, очень сильный комментарий.
Гонку между отменой и исполнением действительно нужно явно формализовать правилом fill wins: отмена относится только к неисполненному остатку, а любое пришедшее исполнение обязано обновить позицию. cancel_requested
нельзя считать финальным состоянием.Особенно забираю идею мониторить не перечень известных аварий, а допущения, на которых держится корректность системы. Список конкретных сбоев всегда будет неполным, а нарушение допущения можно обнаружить независимо от его причины.
Раздельное здоровье каналов тоже намного точнее общего флага «API доступен»: маркет-данные могут поступать, пока размещение уже не работает, либо отмена работает отдельно от новых заявок.
Метрика возможности закрыть портфель прямо сейчас в пределах допустимого проскальзывания действительно сильнее обычного контроля просадки. Стоп смотрит на уже полученный убыток, но ничего не говорит о том, существует ли ещё нормальный выход.
И по биржевым SL/TP согласен: проблема не в том, что стоп превратится в обычную заявку, а в том, что при исчезнувшей ликвидности эта заявка может исполниться с чудовищным проскальзыванием, частично или не исполниться вообще. Стоп фиксирует намерение выйти, но не гарантирует сам выход.
Идея отдельного сторожевого контура, который умеет только отменять заявки и сокращать риск, тоже очень сильная. Особенно с проверкой общей причины отказа основной системы и её защиты.
Спасибо, это уже следующий уровень архитектуры. Если не секрет, при расчёте возможности выхода вы используете текущую глубину стакана как есть или дополнительно стрессуете её исчезновением части объёма?

VsBirdEye
05.08.2026 12:00Этот слой на стадии проектирования, в бою механики попроще (просадка + здоровье фида), так что делюсь замыслом, а не проверенной практикой.
Статический стресс-множитель («допустим, испарится половина объёма») отбросили — его не откалибровать: любой заранее выбранный коэффициент либо слишком мягок для реального хвоста, либо парализует нормальную торговлю. Вместо этого хотим стрессовать измеренной скоростью деградации: не «а что если объём исчезнет», а «объём исчезает вот с такой скоростью, экстраполяция на горизонт выхода даёт вот столько». Сужающаяся дверь важнее уже узкой. Плюс помнить, что собственный выход тоже ест стакан.
Кстати, полез посмотреть T-Invest API — понравился флаг is_consistent в стриме стакана: API сам сознаётся в деградации канала доставки, готовый вход для мониторинга допущений. Еще, в крипте дверь на выход сужает ликвидность, а на фонде её может штатно захлопнуть сама биржа — планка, дискретный аукцион, приостановка торгов. Получаем дополнительный слой рисков.

OreshkinAlexey Автор
05.08.2026 12:00Мысль отказаться от статического стресс-множителя в пользу измеренной скорости деградации выглядит намного здравее. Любой коэффициент вроде «считаем, что исчезнет половина стакана» действительно берётся почти с потолка: в спокойном рынке он будет душить торговлю, а в настоящем хвостовом событии всё равно окажется недостаточным.
Формулировка «сужающаяся дверь важнее уже узкой» здесь попадает точно в цель. Получается, оценивать нужно не только текущую стоимость выхода, но и скорость исчезновения доступного объёма на горизонте, за который позицию реально можно закрыть. Плюс учитывать, что собственная заявка сама съест часть наблюдаемой глубины.
Интересно, как вы планируете выбирать сам горизонт выхода: фиксировать его отдельно для каждого инструмента или рассчитывать динамически из размера позиции, текущей глубины и допустимой доли участия в рынке? И как будете защищаться от нелинейного обвала, когда скорость исчезновения объёма резко ускоряется?
olmernn
Сложно как все... Толи дело МТ или cTrader, кнопку нажал и все
OreshkinAlexey Автор
Да, MetaTrader действительно снимает с разработчика значительную часть этой работы. Там уже есть терминал, готовая модель ордеров, сделок и позиций, торговые события и связь с сервером брокера. Поэтому советник под MT обычно пишется заметно проще, чем отдельное приложение через брокерский API.
Но я бы не говорил, что этих проблем там вообще нет. Даже в MT5 один торговый запрос порождает цепочку транзакций, их порядок не гарантирован, возможны частичные исполнения, а ручные операции пользователя приходят советнику через те же торговые события.
Просто MetaTrader предоставляет готовую инфраструктуру, внутри которой всё это можно обрабатывать. При прямой интеграции с T-Invest API, а особенно с Interactive Brokers, значительную часть инфраструктуры приходится строить самостоятельно.
Я в статье отталкиваюсь именно от максимального уровня ответственности, с которым сталкивался в проектах под IB. Там есть клиенты со счетами в десятки миллионов долларов, и подход «обработаем нормальный сценарий, а остальное маловероятно» уже не работает. При таких суммах приходится проектировать не только момент отправки заявки, но и потерю связи, частичное исполнение, ручное изменение портфеля, перезапуск приложения и восстановление после неопределённого состояния.
Поэтому статья не столько про недостатки T-Invest или преимущества конкретной платформы, сколько про требования к самостоятельной торговой системе, когда терминал не делает половину работы за разработчика.
olmernn
Какие плюсы от T-Invest? Скальпинг или арбитраж разрешают?
OreshkinAlexey Автор
Я здесь не выбирал брокера: робот делался под конкретного клиента Т-Банка. Почему он использует Т-Инвестиции — уже его решение. Скальпинг и арбитраж в этом проекте не применялись, поэтому их регламент отдельно не проверял.