Автоматический стоп для торгового бота
Узнайте, как лимит дневного убытка останавливает торгового бота до усугубления потерь и как масштабирование по волатильности меняет размер позиции.
Circuit breaker для торговых ботов — это правила, которые не позволяют автоматизированной стратегии отправлять заявки после достижения заданного лимита. Они работают на уровне контроля рисков между стратегией и брокером и применяются к каждой заявке независимо от того, согласна с ними стратегия или нет. Стратегия решает, чем торговать. Уровень контроля рисков решает, состоится ли торговля вообще.
На этом разделении строится вся конструкция. Стратегия, которая сама контролирует собственные риски, не имеет независимой проверки в момент, когда её исходные допущения перестают работать. Именно тогда такая проверка особенно необходима.
Что делает circuit breaker в торговом боте
Слой контроля рисков состоит из четырёх частей. Каждая нужна, чтобы предотвратить конкретный, вполне тривиальный сбой.
- Жёсткие ограничения на размер позиции, номинальный объём по каждому тикеру и частоту выставления заявок. Они ограничивают ущерб от ошибки, который иначе мог бы быть неограниченным.
- Circuit breaker по просадке, который прекращает выставление новых заявок, когда счёт теряет за торговую сессию установленную сумму или опускается на установленную величину от пикового значения капитала.
- Масштабирование размера заявки по недавней волатильности или как доли ставки Келли, а не по фиксированному числу акций. Это позволяет примерно сохранять риск на сделку, пока торговый диапазон рынка меняется.
- Журнал аудита в режиме append-only, в котором фиксируется каждое решение, включая заявки, отклонённые слоем контроля рисков. Это единственный артефакт, который позволяет отличить ситуацию «стратегия ошиблась» от ситуации «проверка не выполнялась».
Всё ниже — конкретная реализация одного из этих четырёх элементов.
Какой дневной лимит убытка разумно установить для торгового бота?
Дневной лимит убытка блокирует новые заявки, когда убыток за торговую сессию превышает установленный порог. Выбор значения — это калибровка, а не вопрос вкуса: если установить порог в пределах обычного рыночного шума, бот будет простаивать большую часть недель; если вынести его далеко за эти пределы, он никогда не сработает. Отправная точка — частота, с которой сам рынок показывает снижение определённого масштаба.
Точный SQL-код для каждого числа
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2017-12-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY d
),
with_prev AS (
SELECT d,
close_px,
any(close_px) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_close
FROM daily
)
SELECT toYear(d) AS year,
countIf(close_px / prev_close - 1 <= -0.01) AS down_1pct_days,
countIf(close_px / prev_close - 1 <= -0.02) AS down_2pct_days,
countIf(close_px / prev_close - 1 <= -0.03) AS down_3pct_days
FROM with_prev
WHERE prev_close > 0
AND d >= toDate('2018-01-01')
GROUP BY year
ORDER BY yearКоличество сессий со снижением на 1% и более составило 15 в 2019 и 45 в 2020 при примерно 250 торговых днях в году. Динамика для снижения на 3% и более выглядит иначе: 0 в 2019 против 16 в 2020. Последняя строка 9 охватывает только сессии по 31 июля 2026 года включительно.
Распределение неравномерное, и именно это важно для настройки. Сложные торговые дни приходят сериями. Если бот останавливается в первый день такой серии, а возобновляет работу во второй, фактически он не остановился.
Два порога выполняют разные функции. Дневной лимит убытка, обычно составляющий 2% капитала счёта, завершает торговую сессию. Скользящий лимит просадки, рассчитываемый от максимального значения капитала, обычно устанавливают примерно на уровне 10%; при его достижении стратегия прекращает работу до проверки человеком. Первый порог предназначен для регулярного контроля, второй должен срабатывать редко. Бот, у которого установлен только первый порог, может постепенно сократить счёт на 2% за раз и при этом ни разу не активировать защитное ограничение.
Как масштабирование по волатильности меняет размер позиции?
Таргетирование волатильности определяет размер позиции обратно пропорционально недавней реализованной волатильности: когда дневной диапазон удваивается, позиция примерно уменьшается вдвое, а долларовый риск на сделку остаётся примерно постоянным. В данном случае реализованная волатильность — это годовое стандартное отклонение дневной доходности. Она меняется сильнее, чем многие ожидают.
Точный SQL-код для каждого числа
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2023-12-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY d
),
rets AS (
SELECT d,
close_px / any(close_px) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) - 1 AS ret
FROM daily
)
SELECT formatDateTime(toStartOfMonth(d), '%Y-%m') AS month,
round(stddevSamp(ret) * sqrt(252) * 100, 1) AS realized_vol_pct,
round(least(100.0, 1200.0 / (stddevSamp(ret) * sqrt(252) * 100)), 1) AS vol_target_size_pct
FROM rets
WHERE d >= toDate('2024-01-01')
AND isFinite(ret)
GROUP BY toStartOfMonth(d)
HAVING count() >= 15
ORDER BY toStartOfMonth(d)Реализованная волатильность, измеренная на годовой основе, составила 11.1% в 2024-01 и 12% в 2026-07 за период в 31 месяцев. Во втором столбце каждое значение преобразовано в размер позиции при целевой волатильности 12%, но не выше полной позиции: 100% в 2024-01 против 99.7% в 2026-07. Стратегия та же, уверенность та же, но количество акций существенно отличается.
Критерий Келли подходит к той же задаче с другой стороны. Он определяет размер позиции на основе оценок преимущества и дисперсии, а не только волатильности. Большинство системных управляющих используют лишь долю этого значения — половину или четверть критерия Келли, поскольку оба входных параметра оцениваются по ограниченной выборке. Размер позиции по критерию Келли подробно разбирает эти расчёты.
Почему торговый бот снова входит в позицию после срабатывания стопа?
Стоп срабатывает. Позиция закрывается. Через девяносто секунд условие для входа снова выполняется, бот повторно входит в позицию, получает такой же убыток и продолжает цикл. Ни один отдельный компонент не сломан. Стратегия делает то, для чего её написали, стоп делает то, для чего его настроили, а счёт постепенно теряет средства в каждой отдельной сделке туда и обратно.
Частота таких повторных входов напрямую зависит от движения цены. На этой панели для каждой сессии подсчитывается, сколько раз SPY переходил из зоны более чем на 0,1% выше цены открытия в зону более чем на 0,1% ниже неё и обратно.
Точный SQL-код для каждого числа
WITH mins AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
window_start AS ts,
toFloat64(close) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2025-08-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
),
opens AS (
SELECT d, argMin(px, ts) AS open_px
FROM mins
GROUP BY d
),
zoned AS (
SELECT m.d AS d,
m.ts AS ts,
multiIf(m.px >= o.open_px * 1.001, 1,
m.px <= o.open_px * 0.999, -1,
0) AS zone
FROM mins AS m
INNER JOIN opens AS o ON m.d = o.d
),
flips AS (
SELECT d,
zone,
any(zone) OVER (PARTITION BY d ORDER BY ts ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_zone
FROM zoned
WHERE zone != 0
),
per_day AS (
SELECT d, countIf(prev_zone != 0 AND zone != prev_zone) AS crossings
FROM flips
GROUP BY d
)
SELECT formatDateTime(toStartOfMonth(d), '%Y-%m') AS month,
round(avg(crossings), 1) AS avg_crossings_per_session,
max(crossings) AS max_crossings_in_a_session
FROM per_day
GROUP BY toStartOfMonth(d)
ORDER BY toStartOfMonth(d)SPY пересекал эту границу в среднем 0.7 раза за сессию в 2025-08 и 1.5 раз в 2026-07. В одной из сессий в 2026-07 было зафиксировано 5 пересечений. Любое правило, которое открывает позицию по одну сторону уровня, а закрывает её по другую, получает столько возможностей для срабатывания в течение одного дня.
Четыре механизма ограничивают этот цикл.
- Период ожидания после каждого стопа. Он измеряется в минутах или барах. В это время новый ордер по данному символу не проходит через уровень контроля риска.
- Дневной лимит числа сделок по каждому символу. Он превращает неограниченный цикл в ограниченный.
- Флаг остановки с фиксацией состояния. После срабатывания дневного лимита убытка он остаётся активным, пока его не сбросит оператор.
- Сохранение этого флага за пределами памяти процесса. Если супервизор перезапускает аварийно завершившегося бота, тот получает чистое состояние. Именно этого флаг и должен не допускать.
Последний механизм часто упускают те, кто правильно реализовал все остальные. Боты для grid trading по определению размещают лестницы ордеров, поэтому лимит числа сделок является критически важным элементом, а не формальностью.
Что происходит, когда бот торгует по устаревшему потоку цен?
Котировка, которая перестала обновляться, всё ещё выглядит как число. Бот считывает её, рассчитывает по ней параметры заявки и отправляет эту заявку на уже изменившийся рынок. Сбой происходит незаметно: исключений нет, ошибка не записывается в журнал, а странными исполненные сделки выглядят только постфактум.
Наиболее наглядно это измеряется через ночной гэп: известная цена часами остаётся неизменной, пока цена, по которой можно торговать, меняется.
Точный SQL-код для каждого числа
WITH daily AS (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
argMin(toFloat64(open), window_start) AS open_px,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'KO', 'MSFT', 'AAPL', 'NVDA', 'TSLA')
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2024-01-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY ticker, d
),
gaps AS (
SELECT ticker,
d,
open_px,
any(close_px) OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_close
FROM daily
)
SELECT ticker,
round(quantileDeterministic(0.5)(abs(open_px / prev_close - 1) * 100, cityHash64(ticker, d)), 2) AS median_gap_pct,
round(quantileDeterministic(0.95)(abs(open_px / prev_close - 1) * 100, cityHash64(ticker, d)), 2) AS p95_gap_pct,
round(max(abs(open_px / prev_close - 1) * 100), 2) AS max_gap_pct
FROM gaps
WHERE prev_close > 0
GROUP BY ticker
ORDER BY p95_gap_pct DESCНаибольший гэп на уровне 95-го перцентиля был у TSLA и составил 4.41%, тогда как у KO он достиг 1%. В обычные ночи движения были намного меньше: медианные гэпы составили соответственно 1.01% и 0.24%. Именно для таких хвостовых отклонений и создаётся слой управления рисками. Максимальный разовый гэп по TSLA в измеренном периоде составил 14.57%. Это расстояния, на которые может сместиться рынок, пока бот действует по цене, считанной некоторое время назад. В материале Почему акции образуют ночные гэпы описана механика этого явления.
Защита от такой ошибки не требует больших затрат. Установите максимальный возраст каждой котировки, на основании которой слой управления рисками рассчитывает параметры заявки; для внутридневной стратегии это обычно несколько секунд. Отслеживайте heartbeat потока отдельно от самих данных: это позволяет отличить молчащий сокет от спокойного рынка. Отсутствующие данные следует трактовать как остановку торгов, а не как сохранение последней цены, поскольку без котировок бот не может оценить и свои заявки на выход.
Какие жёсткие лимиты должны относиться к уровню управления рисками?
- Максимальный номинал по одному символу как доля капитала счёта. Лимит в 10% не позволяет одной неудачной позиции поставить под угрозу весь счёт.
- Максимальный совокупный номинал всех открытых позиций. Лимит на уровне 100% капитала означает отсутствие кредитного плеча. Это решение следует принимать явно, а не получать по умолчанию от брокера.
- Максимальная частота выставления заявок за минуту и за день. Десять заявок в минуту — высокий предел для большинства розничных стратегий. При этом он ограничивает последствия зациклившегося алгоритма в течение одной минуты.
- Максимальный размер заявки как доля среднего дневного объёма торгов по символу. Лимит в 1% не позволяет боту заметно сдвигать цену инструмента, по которой он пытается торговать. Средний дневной объём торгов используется в качестве знаменателя.
Все эти ограничения должны относиться к уровню управления рисками, а не к стратегии. Для каждого из них следует использовать один и тот же программный путь в бэктестинге, на бумажном счёте и в реальной торговле. Лимит, который действует только в реальной торговле, никто не тестировал.
Что должен содержать журнал аудита торгового бота?
Журнал только для добавления записей создаёт одну запись на каждое решение и никогда не редактирует и не удаляет её. В каждой записи указываются временная метка, использованная котировка и её возраст, результаты всех выполненных проверок лимитов, отправленный ордер и ответ брокера. Отклонённые ордера записываются с той же полнотой, что и исполненные.
Главная цель — возможность восстановить ход событий. Через шесть недель после неудачной торговой сессии вопрос обычно не в том, каким был P&L. Важно понять, какая проверка прошла и на каких исходных данных. Если исходные данные не записаны, приходится заново восстанавливать состояние бота по состоянию рынка. Это та же ошибка, что и смещение заглядывания вперёд при бэктестинге: использование информации, которой у системы не было в момент принятия решения.
Проект riskguard — один из open-source вариантов такой изоляции. В нём проверки лимитов находятся в отдельном компоненте, к которому обращается стратегия, а не разбросаны по её коду. Это лишь один из возможных вариантов архитектуры. Его стоит изучить, но не следует принимать без проверки. Какую бы зависимость вы ни использовали, зафиксируйте версию с тегом, а не ветку по умолчанию. Ветка может измениться между двумя запусками одного и того же бэктеста. Тихо изменившийся слой управления рисками хуже, чем его отсутствие.
Почему первое развертывание выполняется у бумажного брокера
Адаптер брокера по умолчанию работает в бумажном режиме, а для реальной торговли требуется явно задать соответствующий флаг. Это сделано намеренно. Такой подход предотвращает банальную ошибку: скопированный конфигурационный файл или переменная окружения, которую не переопределили, может отправить реальные заявки и задействовать реальные деньги.
Бумажный запуск также создает важный артефакт — журнал решений из того же уровня управления рисками на реальных ценах. В нем видно, какие лимиты сработали, а какие нет. Это свидетельство о работе уровня управления рисками. Это отдельный вопрос, не связанный с тем, приносит ли стратегия прибыль. Бумажная торговля до использования реальных денег объясняет, что подтверждает бумажный журнал, а чего он не подтверждает, а торговые системы на основе мультиагентного ИИ показывает, почему полномочия по остановке торговли должны находиться вне каждого отдельного агента, если размещать заявки могут несколько агентов.
Часто задаваемые вопросы о защитном выключателе торгового бота
Что такое защитный выключатель в торговом боте?
Это правило уровня управления рисками, которое прекращает отправку новых заявок после достижения установленного лимита. Чаще всего речь идёт о дневном пороге убытка или просадке от максимального значения капитала счёта. Правило работает независимо от стратегии и проверяется перед каждой заявкой. Оно остаётся активным, пока его не сбросят.
Как часто рынок закрывается с падением на 2%?
В 2019 SPY завершил 5 сессий с падением на 2% или более. В 2020 таких сессий было 25 — примерно из 250 торговых дней в каждом году. Разница между спокойным и стрессовым годом показывает, почему лимит убытка следует калибровать по историческим данным, а не по интуиции.
Как не дать боту снова войти в позицию после стоп-сигнала?
Окно охлаждения после каждого срабатывания стопа и дневной лимит числа сделок по каждому символу превращают бесконечный цикл в ограниченный. Флаг остановки также должен фиксироваться и сохраняться за пределами памяти процесса. Иначе супервизор после перезапуска сбойнувшего бота передаст ему новое состояние без активной остановки.
Как бот может определить, что поток котировок устарел?
Перед расчётом заявки бот должен проверять возраст каждой котировки. Отдельно он должен контролировать heartbeat потока, а не только поступающие данные. Ночные разрывы показывают масштаб риска, который скрывает устаревшая цена: 95-й перцентиль ночного движения достиг 4.41% в TSLA в период с января 2024 года по июль 2026 года.
Нужен ли небольшому торговому боту журнал аудита?
Журнал исполненных сделок фиксирует, что произошло. Журнал решений фиксирует, какие действия бот считал разрешёнными. Только так можно отличить ошибку стратегии от непроверенного ограничения риска. Минимально полезный вариант должен быть append-only и включать отклонённые заявки.
Все приведённые выше показатели получены из сохранённого запроса по минутным барам. Каждая панель открывает SQL-код, на котором она основана. Выполните те же запросы для собственного списка символов в терминале Strasmore.