Стоп-заявки
Стоп-заявка лежит у брокера и ждет цены. Пока цена не дошла до заданной, на бирже ее нет. Когда дошла, брокер выставляет обычную заявку, рыночную или лимитную.
Для стоп-заявок есть отдельный трейт StopOrderExecutor. Обычных заявок он не касается, а reconcile и
active_orders стоп-заявок не показывают. Он реализован у всех брокеров (caps().supports_stop_orders).
Виды
Вид (StopOrderKind) | Зачем | Когда срабатывает |
|---|---|---|
StopLoss | защита позиции | продажа - когда цена упала до stop_price, покупка - когда выросла |
TakeProfit | фиксация прибыли | продажа - когда цена выросла до stop_price, покупка - когда упала |
StopLimit | как StopLoss, но после срабатывания всегда лимитная заявка | как у StopLoss, limit_price обязателен |
Параметры (PlaceStopOrderRequest): cid, инструмент, сторона, вид, количество, stop_price (цена срабатывания),
limit_price (цена заявки после срабатывания: нет цены - рыночная), срок (StopExpiry: до отмены, до конца дня,
до даты). Номер cid - тот же UUID, что у обычных заявок, и правила те же: свой на каждую заявку, повторно не
отправляется, заранее записывается в журнал.
Методы
| Метод | Что делает |
|---|---|
place_stop(req) | ставит стоп-заявку, возвращает OrderAck с номером брокера |
cancel_stop(ref) | снимает не сработавшую. ref - либо StopOrderRef::Cid(cid), либо StopOrderRef::Broker(номер) |
active_stop_orders() | список стоп-заявок, которые ждут срабатывания |
subscribe_stop_orders() | поток статусов. У всех брокеров Unsupported: у БКС потока статусов стоп-заявок нет, у Т-Банка потока нет в API, у Финама отдельного потока нет |
Шлюз команд (NotReconciled, Degraded, ShuttingDown) действует на place_stop и cancel_stop так же, как на
place и cancel. Неясный ответ - Unknown, повтора нет. Подробности исходов: Круг команд.
Проверки без сети (нет цены у стоп-лимита, срок, которого нет у брокера, некратное лоту количество) библиотека делает сама до отправки, квота на них не тратится. Узнать о срабатывании стопа можно только опросом: нового события о нем нет. Когда стоп сработал, брокер ставит обычную заявку, и ее ведет обычный поток заявок. Связь между стопом и порожденной заявкой библиотека не строит.
Пример
use brokapi::core::{
ClientOrderId, Error, InstrumentId, PlaceStopOrderRequest, Price, Qty, Side, StopExpiry,
StopOrderExecutor, StopOrderKind, StopOrderRef,
};
/// Ставит стоп-лосс на продажу, смотрит список ждущих и снимает стоп по номеру брокера.
async fn protect<U: StopOrderExecutor>(
ud: &U,
instrument: InstrumentId,
qty: Qty,
stop_price: Price,
) -> Result<(), Error> {
let cid = ClientOrderId::new_v4();
let ack = ud
.place_stop(PlaceStopOrderRequest {
cid,
instrument,
side: Side::Sell,
kind: StopOrderKind::StopLoss,
qty,
stop_price,
limit_price: None, // рыночная заявка после срабатывания
expiry: StopExpiry::GoodTillCancel,
})
.await?;
// Номер брокера нужно сохранить: после рестарта у Т-Банка отмена по cid невозможна
let broker_id = ack.broker_id;
for stop in ud.active_stop_orders().await? {
println!("стоп {:?} {:?} по {}, cid {:?}", stop.kind, stop.state, stop.stop_price, stop.cid);
}
ud.cancel_stop(StopOrderRef::Broker(broker_id)).await
}
Потерянный ответ
Unknown у place_stop значит, что стоп мог дойти до брокера. Тот же cid повторить нельзя, новый стоп наугад ставить тоже: выйдет два.
Прочитайте active_stop_orders():
- Есть стоп с вашим
cid. Он ваш. БКС, Финам, АЛОР и Bybit возвращаютcidсами. Т-Банк нет: его коннектор связывает стоп сcid, если в списке ровно один стоп безcid, который подходит под запрос (бумага, сторона, вид, количество, цены) и создан, пока запрос был в пути. - Есть стопы без
cidна той же бумаге и в ту же сторону, ваш среди них неясен. Второй стоп не ставьте. У Т-Банка так бывает, когда рядом стоит такой же стоп другой программы или два одинаковых ответа потерялись: коннектор не гадает иcidне назначает. - Ничего нет. Стопа у брокера нет. Подождите несколько секунд, посмотрите еще раз (медленный брокер принимает стоп с опозданием), потом
ставьте заново с новым
cid.
Рабочий код этих трех шагов на Python: Стоп-заявки, Python.
Что отличается у брокеров
| БКС | Т-Банк | Финам | АЛОР | Bybit | |
|---|---|---|---|---|---|
| Как ставится | условная заявка (orderType = 5), синхронным методом | отдельный сервис стоп-заявок | стоп-лосс и стоп-лимит обычной заявкой типа стоп, тейк-профит отдельным методом SL/TP | стоп-заявка (рыночная при срабатывании) или стоп-лимит (лимитная), обычным методом с условием срабатывания | обычная заявка с ценой срабатывания (triggerPrice), рыночная при срабатывании или лимитная (StopLimit) |
Тейк-профит с ценой после срабатывания (limit_price) | да (лимитное исполнение) | да | нет: у Финама там защитный спред, а не цена. Вернется Rejected с кодом unsupported_limit_price | да (стоп-лимит) | да (StopLimit или вид с limit_price) |
| Срок «до отмены» | да | да | да | да (уходит как год вперед: брокер требует время окончания) | да (единственный) |
| Срок «до конца дня» | нет, unsupported_expiry | нет, unsupported_expiry | да | да (конец дня по Москве) | нет, unsupported_expiry |
| Срок «до даты» | нет, unsupported_expiry | да | только у тейк-профита | да | нет, unsupported_expiry |
| Количество | штуки, целое не меньше 1 | штуки, кратные лоту (коннектор переводит в лоты, иначе invalid_qty) | штуки, число больше нуля (кратность лоту проверяется, если лот передан) | штуки, кратные лоту (коннектор переводит в лоты, иначе invalid_qty) | базовая монета, кратная шагу контракта (иначе invalid_qty) |
Свой cid в списке | находится одним запросом по номеру заявки, если брокер знает условные заявки | только пока работает процесс, который поставил заявку. Брокер cid не возвращает, связь хранится в памяти. После рестарта cid пустой. Исключение: стоп с потерянным ответом получает cid, если подходит ровно один | да, читается из комментария заявки | читается из комментария заявки, после перезапуска тоже | находится в списке по orderLinkId, свой cid возвращается сам |
| Поток статусов | нет | нет в API | нет | нет | нет, и после срабатывания стоп приходит в потоке заявок как обычная заявка под тем же cid |
Особенности:
- БКС: срок жизни. Документация срок условной заявки не называет. Поэтому БКС принимает только «до отмены» и никакого поля срока не шлет. Срок жизни условной заявки в документации БКС не назван. Направление срабатывания БКС выбирает сам из стороны и вида. Пару «стоп-лосс плюс тейк-профит» одной заявкой библиотека не создает. Чужую пару в списке показывает двумя заявками с одним номером.
- Т-Банк: отмена после рестарта.
cancel_stop(StopOrderRef::Cid(cid))для стопа, поставленного раньше этого запуска, вернетRejectedс кодомunknown_stop_order. Снимайте поStopOrderRef::Broker(номер): номер берите из журнала бота или изactive_stop_orders(). - Bybit: списки. Ожидающие стоп-заявки не попадают в
active_ordersиreconcileкак рабочие заявки, для нихactive_stop_orders(после срабатывания стоп становится обычной заявкой). Позицию стоп сам не закрывает:reduceOnlyиcloseOnTriggerконнектор не ставит, поэтому размер стопа равен позиции. Подробно: Bybit. - Финам: списки. Стоп-заявки не попадают в
active_ordersиreconcile. Для них свой список. - Финам: процент вместо штук. Если у чужой SL/TP-заявки единица объема - процент от позиции, в
qtyбудет 0 («не в штуках»), а не процент. Свои заявки библиотека ставит в штуках. - Т-Банк: длинный список. Когда стоп-заявок слишком много для одного ответа (ошибка 30261),
active_stop_ordersчитает список по периодам и склеивает. Если и так не вышло,Rejectedс кодомstop_orders_response_limit. Подробно: Т-Банк. - Т-Банк: лимит по инструменту. Ответ 80007 (максимум стоп-заявок по инструменту) - это
Rejectedс кодомstop_orders_limit, а неRateLimited: повтор не поможет, снимите одну из заявок. - Сверка и стопы.
cidстоп-заявок в список дляreconcileне кладите: обычной сверкой они не видны. После старта читайтеactive_stop_orders().
Стоп-заявки проверяйте на минимальном объеме с ценой далеко от рынка.
Листать можно клавишами Влево и Вправо.