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

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

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

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

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

Круг жизни: шлюз и сверка

После запуска библиотека не знает, какие заявки у бота были раньше: она ничего не хранит на диске. А на счете могут стоять заявки с прошлого запуска, заявки, которые исполнились, пока бот был выключен, и заявки, поставленные руками. Сверка (reconcile) разбирает это и только после нее открывается шлюз торговых команд. Это третий круг механики: он решает, можно ли сейчас торговать. Схема трех кругов вместе - на Карте.

Шлюз команд

У каждого UD есть шлюз торговых команд (CommandGate). Чтение работает всегда: order_state, active_orders, executions, portfolio, подписки. Закрывается только торговля: place, cancel, replace, place_stop, cancel_stop возвращают ошибку и в сеть не уходят. На схеме: состояния шлюза и переходы между ними.

stateDiagram-v2
  state "Не сверен<br>NotReconciled" as NR
  state "Торговля открыта" as OPEN
  state "Перегрузка<br>Degraded" as DEG
  state "Остановка<br>ShuttingDown" as SD
  [*] --> NR: start
  NR --> OPEN: reconcile успешен
  OPEN --> DEG: Overflow в Lossless-буфере
  DEG --> OPEN: бот дочитал канал и reconcile
  OPEN --> SD: shutdown
  NR --> SD: shutdown
  DEG --> SD: shutdown
  SD --> [*]

Шлюз закрыт в трех случаях, и у каждого своя причина:

  • NotReconciled: после запуска. Пока бот не знает, какие заявки уже стоят у брокера, новая команда может задвоить заявку или спорить с той, которую бот не видит.
  • Degraded: переполнился буфер подписки без потерь. События о своих заявках могли пропасть, и локальная картина заявок с дырой. Торговать при дыре нельзя (Круг событий).
  • ShuttingDown: идет остановка. Выхода нет, коннектор закрывается (Запуск, рестарт и остановка).

Если закрыто несколько условий одновременно, первым проверяется ShuttingDown, затем Degraded, затем NotReconciled. Успешная сверка снимает NotReconciled и Degraded. Из обоих закрытых состояний выход один - сверка. Что делает каждая ошибка шлюза и как из нее выйти: Ошибки и что с ними делать.

Что бот передает и что получает

Бот передает список cid заявок, которые он считает живыми (открытыми или с неизвестным исходом). Получает ReconcileReport с тремя списками:

СписокЧто в немЧто делать
confirmedзаявки, которые есть и у бота, и у брокера. Пришли целиком (OrderSnapshot): состояние, исполнено, ценаПродолжать вести. Обновить в журнале состояние и исполненный объем
closed_offlinecid из вашего списка, которых нет среди живых у брокера: заявка исполнилась, снята, отклонена или так и не была принята, пока бот был выключенВыяснить итог: order_state(cid) и история executions(...). Занести итог в журнал
foreignзаявки, которые есть у брокера, но нет в вашем списке: поставлены руками, из приложения или другим роботомНе трогать. Не снимать и не менять. Решение, что с ними делать, принимает человек

У чужих заявок cid подставной: брокер вашего номера не знает. Он стабильный (один и тот же для одной заявки) и выглядит как UUID, но выдан библиотекой из номера брокера, а не вами. Почему foreign нельзя трогать: бот не знает, зачем человек поставил эту заявку, и снятие может сломать чужую стратегию или защитный стоп.

В коде порядок такой. Подписка идет до сверки, сверка принимает список из журнала, три списка разбираются каждый по-своему:

// Second run, after a restart. The gate is closed after `start`: commands return NotReconciled.
let (ud, mut events) = start_ud(cfg).await?;
// Subscribe BEFORE the reconcile: an event between the snapshot and the subscription would be lost.
ud.subscribe_order_state().await?;

// The journal says which orders the bot thinks are alive.
let report = ud.reconcile(&journal).await?;
for order in &report.confirmed {
    println!(
        "confirmed {}: still at the broker, filled {}",
        order.cid,
        order.filled.decimal()
    );
}
for cid in &report.closed_offline {
    // Find out how it ended: order_state(cid) and executions(...), then write it to the journal.
    println!("closed offline {cid}: ask order_state and write the result");
}
for order in &report.foreign {
    // Not ours. Do not touch: a person decides.
    println!("foreign {}: shown to a person", order.cid);
}
# After a restart the gate is closed: commands raise NotReconciled.
# Subscribe BEFORE the reconcile: an event between the snapshot and the subscription would be lost.
await client.subscribe_order_state()

# The journal says which orders the bot thinks are alive.
report = await client.reconcile(ledger)
for order in report.confirmed:
    print(f"confirmed {order.cid}: still at the broker, filled {order.filled}")
for cid in report.closed_offline:
    # Find out how it ended: order_state(cid) and executions(...), then write it to the journal.
    print(f"closed offline {cid}: ask order_state and write the result")
for order in report.foreign:
    # Not ours. Do not touch: a person decides.
    print(f"foreign {order.cid}: shown to a person")

Первая торговая команда идет только после этого. Сначала подписка, потом сверка, потом заявка, а до сверки заявка получает NotReconciled:

ud.subscribe_order_state().await?; // before the reconcile, or an event in between is lost
let report = ud.reconcile(&journal.live()).await?; // an empty slice for a bot without a journal
// report.confirmed, report.closed_offline, report.foreign: handle them (see the lifecycle page)
ud.place(request).await?; // before the reconcile this is Error::NotReconciled
await client.subscribe_order_state()  # before the reconcile, or an event in between is lost
report = await client.reconcile(journal.live())  # an empty list for a bot without a journal
# report.confirmed, report.closed_offline, report.foreign: handle them (see the lifecycle page)
await client.place(cid=brokapi.new_cid(), instrument=instrument, side=brokapi.Side.BUY, qty=qty, price=price)
# Before the reconcile the line above raises brokapi.NotReconciled.

Порядок на схеме такой: бот поднимает UD (шлюз закрыт), подписывается на статусы, берет из журнала cid живых заявок, зовет reconcile, получает три списка, шлюз открывается, итоги идут в журнал.

%%{init: {'sequence': {'width': 90, 'actorMargin': 30, 'mirrorActors': false}}}%%
sequenceDiagram
  participant J as Журнал бота
  participant Bot as Бот
  participant UD as UD, коннектор счета
  participant Br as Брокер
  Bot->>UD: start, шлюз закрыт
  Bot->>UD: подписка на заявки и сделки
  J->>Bot: cid живых заявок
  Bot->>UD: reconcile(cid)
  UD->>Br: заявки счета
  Br-->>UD: список
  UD-->>Bot: три списка
  Note over UD: шлюз открыт
  Bot->>J: итоги в журнал

Если reconcile вернул ошибку (нет связи, лимит), он не открыл шлюз: подождите и вызовите еще раз. Ошибка при этом ничего не говорит о заявках, сверка просто не состоялась. Пока шлюз закрыт, ставить заявки нельзя, и это правильно: бот все еще не знает, что стоит на счете.

Почему порядок именно такой

Пошаговая инструкция - на странице Запуск, рестарт и остановка бота. Здесь причины:

  • Подписка идет до сверки. Сверка делает снимок счета в одно мгновение. Событие, которое пришло между снимком и подпиской, иначе было бы потеряно: в снимке его еще нет, а подписка его уже не получит. Подписки при закрытом шлюзе разрешены именно поэтому.
  • Список cid берется из журнала бота. Только бот знает, какие заявки он считал живыми. Библиотека не пишет на диск, и без этого списка она не отличит ваши заявки от чужих.
  • reconcile только читает, поэтому его можно вызывать в любой момент и сколько угодно раз. Он не снимает и не меняет заявок. Единственное, что он меняет, - состояние шлюза.
  • Сверка не открывает шлюз при ошибке. Если часть списка не прочиталась, результат нельзя считать полным.

Как сверка устроена у брокеров

БрокерКакНюансы
Т-Банкодин запрос списка живых заявок счета. cid из вашего списка, которого среди живых нет, идет в closed_offlineв списке нет заявок, закрытых за время обрыва, их находят через order_state и executions
БКСрешает ответ на GET по каждому cid вашего списка, не список заявок. Заявка жива (новая, частично исполнена, заменяется, отменяется): confirmed, со снимком из этого ответа. Заявка закончилась (исполнена, отменена, отклонена) или брокер не знает номер (not_found): closed_offline. Затем читается список активных заявок, из него берутся только foreignпустой ответ без отчета и любая другая ошибка по одному из cid прерывают всю сверку: по неясному ответу заявку «закрытой» не объявляют, иначе бот поставит вторую. Что настоящий БКС отвечает на неизвестный cid именно not_found, проверить на живом счете
Финамодин запрос списка заявок счета, cid каждой заявки читается из ее комментария. Не найденный в списке cid и найденный, но не активный, идут в closed_offlineзависит от того, что Финам возвращает комментарий заявки без изменений и что отдает GetOrders для закрытых заявок
АЛОРдва запроса: список заявок дня и список стоп-заявок. cid каждой заявки читается из ее комментария. cid из вашего списка, которого нет в списках, и найденный закрытый идут в closed_offline, работающая стоп-заявка идет в confirmedзаявки прошлых дней, если они не живы, в списке дня не видны: такой cid попадает в closed_offline, итог закрытой заявки читает order_state или executions. Номер, которого брокер не знает, не запрашивается
Bybitодин запрос списка рабочих заявок счета, cid каждой читается из orderLinkId. cid из вашего списка: жив (в том числе ожидающая стоп-заявка) - confirmed, иначе поиск по orderLinkId в списке и в истории: закрыта или не найдена - closed_offline. Чужие рабочие заявки - foreignотмененную заявку без исполнений биржа держит в истории 24 часа, дальше она не находится, и reconcile относит ее к closed_offline («не видел сегодня»), как у остальных брокеров

Список БКС ограничен: в нем только заявки с 26 января 2026 года и не больше 1000 последних (ограничение документации БКС). Поэтому живая заявка может в нем не оказаться, и именно поэтому судьбу заявки из вашего списка решает GET, а список нужен только для foreign. Окно поиска списка заканчивается на сутки позже локальных часов: часы машины могут отставать от брокерских, и свежая заявка не должна выпасть.

Стоп-заявки

Обычная сверка стоп-заявки не показывает: ни в confirmed, ни в foreign. cid стоп-заявок в ваш список для reconcile класть не надо: они попадут в closed_offline, потому что среди обычных живых заявок их нет. Для стопов после старта вызывайте active_stop_orders() (см. Стоп-заявки). У Т-Банка после рестарта cid в этом списке пустой, снимать стоп можно только по номеру брокера.

Что хранит бот, а что нет

ЧтоГде хранится
Журнал заявок: cid, состояние, номер брокера, исполненобот
Какие заявки считались живыми на момент остановкибот (из журнала)
Номера брокера стоп-заявок Т-Банкабот, если нужна отмена после рестарта
Связь cid и номера брокера, состояние заявок текущего запускабиблиотека, в памяти, после остановки исчезает
Подпискибиблиотека, в памяти. После рестарта бот подписывается заново

Библиотека не ведет журнал на диске и не восстанавливает состояние сама. Она дает основу для этого: полный упорядоченный поток событий (каждый переход состояния заявки - событие), cid в каждом событии заявки и сверку, которая сравнивает список бота со счетом.

Когда звать reconcile, кроме старта (после Overflow, после долгого обрыва, при сомнении в журнале): Если что-то пошло не так.

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

brokapi by RunInfra