Горячие клавиши

← и →: предыдущая и следующая глава

S или /: поиск по книге

?: показать эту подсказку

Esc: закрыть подсказку или поиск

Стоп-заявки

Стоп-заявка лежит у брокера и ждет цены. Пока цена не дошла до заданной, на бирже ее нет. Когда дошла, брокер выставляет обычную заявку, рыночную или лимитную.

Для стоп-заявок есть отдельный трейт 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().

Стоп-заявки проверяйте на минимальном объеме с ценой далеко от рынка.

Листать можно клавишами Влево и Вправо.

brokapi by RunInfra