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

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

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

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

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

Контракт для агентов

Все обязательные правила brokapi на одной странице. Страница для ИИ-агента, который пишет код на библиотеке, и для человека, который этот код проверяет. Нарушение правила не дает ошибки компиляции. Оно дает двойную заявку, торговлю по чужой картине счета или отказы брокера, поэтому правила строгие.

Для ИИ-агентов: llms.txt и llms-full.txt (вся книга одним файлом).

У каждого правила пять частей: правило, почему, что сломается при нарушении, как правильно (код) и ссылка на подробное объяснение. Код в «как правильно» собирается вместе с библиотекой (examples/contract.rs), поэтому он не отстает от API. Целые рабочие программы лежат в инструкциях: торговый бот, несколько брокеров и счетов, исследовательский движок.

Правило 0: токен и первая боевая заявка только от человека

Правило. Это правило выше всех остальных, и оно про человека, а не про код.

  • Боевой токен выдает человек, чей это счет. ИИ-агент не заходит в кабинет брокера и сам не выпускает токен с правом торговли.
  • Флаги записи (BCS_IS_WRITE=true, write=True в connect, токен «для торговли и чтения») агент сам не включает. Их включает человек, когда решил.
  • Первую заявку на боевом счете агент ставит только со слова человека, на конкретный счет и конкретную заявку («можно, счет такой-то, одна заявка, столько-то»). Слово «можно» в начале сеанса на все последующие заявки не распространяется.
  • До этого слова агент работает на имитациях (brokapi.testing, brokapi-mocks), потом на песочнице Т-Банка. Боевой счет читают токеном только для чтения, если человек такой выдал.

Почему. Заявка на биржу - это деньги человека, и откатить ее нельзя. Токен с правом торговли равен доступу к счету. Библиотека проверяет порядок вызовов, но не может знать, хотел ли владелец счета эту заявку.

Что сломается. Агент, включивший запись «чтобы проверить», ставит настоящую заявку на настоящий счет. Ошибка в цене, количестве или счете стоит денег, а не упавшего теста.

Как правильно. Написать и проверить код на имитации, остановиться перед боевым счетом и попросить человека. Что попросить, можно отдать ему целиком:

Прошу у владельца счета:

  1. Токен песочницы Т-Банка и открытый, пополненный счет песочницы (для первого прогона).
  2. Токен только для чтения для боевого счета, если брокер такие выпускает (БКС, Т-Банк). Где его взять: раздел «Вход» на странице брокера в книге.
  3. Номер счета (портфеля): TBANK_ACCOUNT_ID, FINAM_ACCOUNT_ID, ALOR_PORTFOLIO. У БКС счет определяет сам токен, у Bybit ключ и секрет API (лучше пара демо-счета или отдельного субсчета без права вывода).
  4. Решение о первой боевой заявке письменно: счет, инструмент, сторона, количество, предельная цена, «можно».
  5. Только после пункта 4: токен с правом торговли и включенная запись.

Подробнее: Перед боевым счетом, Аварийная остановка бота, разделы «Вход» на страницах брокеров: БКС, Т-Банк, Финам, АЛОР.

Порядок любой программы

flowchart TB
  A[Шаг 1: один BrokerLimiter на область квоты,<br>один канал событий] --> B["Шаг 2: start, MD (рынок) на брокера,<br>UD (счет) на счет"]
  B --> C[Шаг 3: subscribe_order_state<br>и subscribe_executions]
  C --> D[Шаг 4: reconcile со списком cid из журнала]
  D --> E[Шаг 5: разобрать confirmed, closed_offline, foreign]
  E --> F[Шаг 6: работа, события в одной задаче,<br>команды из другой]
  F --> G[Шаг 7: остановка, свои заявки снять,<br>если так решил бот, потом shutdown]

На схеме: шаги, которые делает любая программа на brokapi, в обязательном порядке. Пункты 3 и 4 нельзя менять местами, пункт 4 нельзя пропустить: без него любая торговая команда вернет NotReconciled.

Коротко

№Правило
0Боевой токен выдает человек. Первую боевую заявку ставят только с его слова. Токен с правом торговли и флаги записи агент сам не создает и не включает
1cid придумывает бот и пишет в журнал на диск до place
2Торговля только после reconcile. Подписка на статусы - до него
3Unknown не повторять. Выход: order_state, reconcile. Новая попытка - с новым cid
4Один BrokerLimiter на область, где брокер считает лимит. Ждать квоту должен бот
5Свои заявки и сделки идут только Lossless. Канал читать всегда, отдельной задачей
6Overflow, Reconnected и Degraded: дочитать канал, прочитать состояние, reconcile, восстановить пропущенное
7Resync: стакан выбросить, не торговать по нему
8Цены, количества и деньги - Decimal (Price, Qty, Money). Цену округлять до шага. Количество в единицах инструмента, не в лотах, кратно лоту
9Один UD на счет, один MD на брокера. Идентификатор инструмента - вид своего брокера
10shutdown не снимает заявки. Снимает или не снимает бот
11Чужие заявки (foreign) не трогать
12Длинную историю свечей читать окнами (CandlePages), не одним запросом
13Возможность, которой нет у брокера, это Unsupported, а не сбой. Проверять до вызова

1. cid придумывает бот и пишет в журнал до place

Правило. Номер заявки (ClientOrderId, UUID) создает бот: ClientOrderId::new_v4(). Он записывает его в свой журнал на диск (с принудительной записью, fsync) до вызова place. Один cid - одна заявка. Новая попытка - новый cid.

Почему. Библиотека ничего не хранит между запусками. Если ответ на place потерялся, cid - единственная нить, по которой после падения можно спросить брокера «что с заявкой». Журнал, записанный после вызова, теряет заявку, когда процесс падает между вызовом и записью.

Что сломается. Без записи до вызова после падения бот не знает о заявке, которая могла встать, и не сверит ее (closed_offline и confirmed строятся из вашего списка). Повтор того же cid библиотека не отправит: Rejected с кодом duplicate_place, единица квоты потрачена.

Как правильно.

let cid = request.cid; // made by the bot: ClientOrderId::new_v4()
journal.sending(cid)?; // on disk BEFORE the call
let result = ud.place(request).await;
cid = brokapi.new_cid()  # made by the bot
journal.sending(cid)  # on disk BEFORE the call
result = await client.place(cid=cid, instrument=instrument, side=brokapi.Side.BUY, qty=qty, price=price)

Подробнее: Круг команд, cid, Запуск, рестарт и остановка.

2. Торговля только после reconcile

Правило. После start шлюз торговых команд закрыт. Порядок: подписаться на статусы и исполнения, вызвать reconcile(&журнал), только потом ставить заявки. Если журнала нет, список пустой: reconcile(&[]).

Почему. Пока бот не знает, что уже стоит у брокера, новая команда может задвоить заявку или спорить с той, которую бот не видит. Подписка идет раньше сверки, потому что событие между снимком сверки и подпиской иначе теряется. subscribe_* возвращается, когда поток открыт, поэтому заявку можно ставить сразу после него: исполнение, случившееся после возврата, до бота дойдет. Не открылся за subscribe_ack_timeout (3 секунды) - Transport, подписка снята, вызов можно повторить. Исключение Финам: по таймауту Ok и поток дооткрывается в фоне (sub_status), подробно в Круге событий.

Что сломается. place, cancel, replace, place_stop и cancel_stop возвращают NotReconciled, в сеть ничего не уходит. cid при этом не потрачен. Если подписаться после заявки, события о ней потеряны.

Как правильно.

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.

reconcile только читает, его можно вызывать когда угодно и сколько угодно. Ошибка reconcile значит, что сверки не было и шлюз закрыт: подождать и повторить.

Подробнее: Круг жизни, Запуск, рестарт и остановка.

3. Unknown не повторять

Правило. Если команда вернула Error::Unknown (или Error::Transport на команде), заявка могла встать. Команду не повторять. Спросить брокера по тому же cid: order_state(cid), при сомнении active_orders, executions, reconcile. Новую заявку ставить с новым cid, только когда стало ясно, что первой нет.

Почему. Из «нет ответа» нельзя понять, дошла ли заявка. Повтор при первой дошедшей дает две заявки на счете, для денег это худший исход. Библиотека сама никогда не повторяет команды, и бот тоже не должен.

Что сломается. Двойная позиция. Отрицательный ответ сразу после обрыва тоже ничего не доказывает: незнакомый номер приходит как Rejected с кодом not_found (один код у всех), и запрос, который был в пути, может дойти позже. Подождать и спросить еще раз.

Как правильно.

let cid = request.cid;
match ud.place(request).await {
    Ok(_) | Err(Error::Rejected { .. }) => {}
    Err(Error::Unknown | Error::Transport(_)) => {
        // The order may exist. NEVER place again. Ask about THE SAME cid (see the recovery page for the loop).
        let _state = ud.order_state(cid).await;
    }
    Err(other) => return Err(other.into()),
}
cid = brokapi.new_cid()
state = None
try:
    await client.place(cid=cid, instrument=instrument, side=brokapi.Side.BUY, qty=qty, price=price)
except brokapi.Rejected:
    pass  # the broker said no: there is no order
except (brokapi.Unknown, brokapi.Transport):
    # The order may exist. NEVER place again. Ask about THE SAME cid (see the recovery page for the loop).
    state = await client.order_state(cid)

Как брокеры отвечают на номер, которого не знают (order_state):

БрокерОтвет
БКСError::Rejected с кодом not_found
Т-БанкError::Rejected с кодом not_found
ФинамError::Rejected с кодом not_found
АЛОРError::Rejected с кодом not_found (список заявок дня просматривается, номера, которого нет, брокеру не отправляется: запрос несуществующей заявки он штрафует)
BybitError::Rejected с кодом not_found (код биржи 110001)

Error::Transport на команде rustdoc описывает как «сбой до отправки», но коннектор не всегда может это доказать, поэтому считайте, что заявка могла дойти, и спросите order_state.

Сколько ждать. Сразу после Unknown в том же запуске запрос еще мог быть в пути: спрашивайте несколько секунд, сдавайтесь по истечении терпения. После рестарта (closed_offline из сверки) запроса от прежнего процесса уже нет в пути: «не найдена» после короткого ожидания окончательно, запись в журнале закрывается как «брокер не знает». В примерах полсекунды для имитаций, для боевого счета берите секунды.

Цикл «спросить, подождать, спросить еще раз» с готовым кодом: Если что-то пошло не так. Для cancel с Unknown повторить можно после проверки order_state. Для replace спрашивать по обоим номерам.

Подробнее: Почему нет автоповтора, Ошибки: Unknown.

4. Один BrokerLimiter на область квоты

Правило. Счетчик лимитов (BrokerLimiter) создается один на ту область, где брокер считает квоту, и передается всем коннекторам этой области. Область у брокеров своя.

БрокерОбласть квотыСчетчиков
БКСтокен (то есть счет)по одному на токен
Т-Банкпользователь: все счета и все токены вместеодин на программу
Финампользовательодин
АЛОРлогин (все портфели вместе)один на логин
Bybitключ (пользователь и метод) и адрес (600 запросов за 5 секунд)один на ключ, а если с одного адреса работают разные ключи, адресный лимит общий: держите ключи одной программы на одном BrokerLimiter

Разные брокеры счетчик не делят.

Почему. Два счетчика на одну квоту вместе ее превысят, и брокер начнет отказывать, в самый неудобный момент, в том числе на отмене. Один счетчик там, где лимиты независимы, лишний: коннекторы будут мешать друг другу.

Что сломается. RateLimited от брокера на боевом счете. Библиотека сама не ждет квоту: при нехватке сразу RateLimited { retry_after }, в сеть ничего не ушло. Ждать должен бот. Квоту тратит любой вызов: find, last_trades, trading_params, подписка, а не только place и свечи. Исключение одно: CandlePages ждет сам.

Как правильно.

// T-Bank counts per user: the market data and BOTH accounts share one limiter.
let limiter = BrokerLimiter::new(&brokapi::tbank::limits::default_limits());
let (tx, _events) = mpsc::channel::<Envelope>(8192);
let md =
    brokapi::tbank::MarketDataConnector::start(md_cfg, limiter.clone(), tx.clone()).await?;
let a = brokapi::tbank::UserDataConnector::start(ud_a_cfg, limiter.clone(), tx.clone()).await?;
let b = brokapi::tbank::UserDataConnector::start(ud_b_cfg, limiter, tx).await?;
if let Err(Error::RateLimited { retry_after }) = ud.place(request).await {
    // Nothing was sent. Wait; the next try gets a NEW cid (always safe).
    tokio::time::sleep(retry_after).await;
}

Подробнее: Лимиты запросов, Несколько счетов и брокеров.

5. Свои заявки и сделки идут только Lossless, канал читать всегда

Правило. Статусы своих заявок и свои сделки (subscribe_order_state, subscribe_executions) приходят только в режиме Lossless: библиотека ставит его сама, размер буфера - order_state_buffer в настройке UD (1024). Режим для стакана и котировок - Conflate, для ленты сделок рынка - DropNewest или Lossless. Канал событий читается всегда, отдельной задачей, которая ничего не ждет: она только переносит конверты в очередь бота.

Почему. Поток брокера нельзя замедлить, и потерянное событие о частичном исполнении значит, что бот торгует по неверной картине. Поэтому для своих событий дыра не молчит: буфер переполняется громко (см. правило 6).

Что сломается. Если задача, которая читает канал, ждет команду или диск (а place ждет ответ брокера, до 30 секунд), буфер заполняется. Приходит Overflow, торговля закрывается (Degraded). Канал событий делайте с запасом: в примерах книги от 1024 до 8192.

Как правильно. Читатель канала переносит конверты в собственную очередь, бот разбирает очередь и может ждать команды сколько нужно. Очередь без границы: следите за ее длиной, растущая очередь значит, что бот отстает.

// The reader does nothing but move envelopes to the bot's own queue, so the library's channel is always empty.
// The bot takes envelopes from `queue` and may await commands as long as it needs.
let (queue_tx, queue) = mpsc::unbounded_channel();
tokio::spawn(async move {
    while let Some(env) = events.recv().await {
        if queue_tx.send(env).is_err() {
            break;
        }
    }
});

Подписка на стакан:

let sub = md
    .subscribe_order_book(
        id,
        SubscribeOpts {
            depth: Some(10),
            ..SubscribeOpts::conflate()
        },
    )
    .await?;
// events of this subscription: env.source (which connector) + env.sub == Some(sub)

Подробнее: Круг событий.

6. Overflow, Reconnected и Degraded: дочитать, сверить, восстановить

Правило. Пришел Event::Overflow или Event::Reconnected (поток своих заявок или исполнений оборвался и открыт снова) или команда вернула Error::Degraded. По порядку: дочитать канал до пустого (это история до дыры), вызвать reconcile(&журнал), восстановить пропущенное из снимков (состояния из confirmed и order_state, сделки из executions(range) начиная с lost_from (у Reconnected с запасом раньше gap_from), портфель из portfolio()), вернуться к работе. Для рыночной подписки сверки нет: подписку снять (unsubscribe) и оформить заново.

Почему. Торговать при дыре в данных о своих заявках нельзя: бот мог пропустить исполнение или снятие. Шлюз закрыт, пока бот не сверит картину со счетом.

Что сломается. Без сверки каждая торговая команда возвращает Degraded. Если сверку сделать до того, как дочитан канал, события до дыры будут обработаны уже после снимка и могут откатить состояние назад.

Как правильно.

while let Ok(env) = events.try_recv() {
    let _ = env; // the events that DID arrive are the history up to the hole: handle them as always
}
let report = ud.reconcile(&journal.live()).await?; // opens the gate again
// then restore: report.confirmed (states), executions(range) since lost_from, portfolio()

Подробнее: Если что-то пошло не так, Круг событий, Reconnected.

7. Resync: стакан выбросить

Правило. Event::Resync { instrument, .. } значит, что хранимому стакану этого инструмента верить нельзя. Выбросить его, не торговать по нему, дождаться следующего Event::OrderBook.

Почему. После обрыва поток переоткрыт, часть данных могла пропасть. Стакан у всех брокеров приходит полным снимком, поэтому следующий снимок сам все исправит.

Что сломается. Заявка по цене из устаревшего стакана.

Как правильно.

match event {
    Event::Resync { instrument, .. } => {
        books.remove(&instrument.key); // do not trade from it; the next snapshot replaces it
    }
    Event::OrderBook(book @ OrderBook::Snapshot { .. }) => {
        if let OrderBook::Snapshot { instrument, .. } = &book {
            books.insert(instrument.key.clone(), book.clone());
        }
    }
    _ => {}
}

Обрыв потока стакана бот видит как Resync. Обрыв потока своих заявок или исполнений приходит как Event::Reconnected: шлюз при этом закрыт (Degraded), см. правило 6.

Подробнее: Круг событий.

8. Цены и количества - Decimal, цена на шаге

Правило. Price, Qty и Money построены на Decimal. f64 для денег, цен и количеств не использовать нигде. Цену округлять до шага цены инструмента (TradingParams::price_step): покупка вниз (RoundMode::Down), продажа вверх. Шаг и лот передать коннектору один раз: price_grid().learn(&params). Количество заявки - в единицах инструмента (штуки, контракты, единицы валюты), одинаково у всех брокеров, и кратно лоту (TradingParams::lot_size). Некратное, дробное там, где брокер берет целые, и нулевое количество это Rejected с кодом invalid_qty до отправки. Ничего не округляется. Т-Банк считает лотами, и его коннектор переводит туда и обратно сам.

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

Что сломается. Цена не на шаге дает Rejected с кодом price_off_step до отправки (квота и cid не тратятся), если шаг коннектору известен. Если не известен, решает брокер, и отказ придет от него. Количество, которое не целое число лотов, дает invalid_qty без отправки (у Т-Банка, АЛОРа и Bybit всегда, у БКС и Финама когда лот передан).

Как правильно.

// 1% below the bid, rounded DOWN to the price step (for a sell round UP). No f64 anywhere.
let wanted = best_bid * Decimal::new(99, 2);
let price = wanted
    .round_to_step(params.price_step, RoundMode::Down)
    .map_err(|e| Error::Config(ConfigError::Message(e.to_string())))?;
price_grid.learn(params); // once per instrument; then place() refuses a price off the step before sending

Подробнее: Деньги, цены и количества.

9. Один UD на счет, один MD на брокера, идентификатор своего брокера

Правило. Заявки и портфель идут через UD, по одному на счет. Рынок идет через MD, по одному на брокера. InstrumentId несет NativeId, и вид у каждого брокера свой (БКС: тикер и класс, Т-Банк: uid, он основной, но принимаются figi и тикер с классом, Финам: символ SBER@MISX). Получать идентификатор через find или собирать по странице брокера.

Почему. MD и UD разделены, чтобы всплеск стакана не задерживал отмену заявки. Один UD не умеет работать с двумя счетами.

Что сломается. Идентификатор чужого вида дает Error::Config, вызов не делается. Общий портфель по счетам и выбор счета для заявки в библиотеке нет, это слой бота.

Подробнее: Несколько счетов и брокеров, Брокеры.

10. shutdown не снимает заявки

Правило. shutdown(режим) останавливает коннектор и забирает его по значению. Заявки на бирже он не трогает. Хочет бот снять свои заявки перед выходом, снимает их сам и до shutdown. Порядок: сначала UD (команды закрыты), потом MD.

Почему. «Снять все» может быть неверным решением: например, защитный стоп должен пережить бота.

Что сломается. Заявка, которую бот считал снятой, остается на бирже и после рестарта приходит в confirmed сверки.

Как правильно.

for cid in working {
    // Ok(()) means cancelled. Rejected: already final. Unknown: ask order_state, cancel again only if it works.
    let _ = ud.cancel(*cid).await;
}
// Then the connector: `shutdown` takes it by value. Account connectors first, market data last.
// ud.shutdown(ShutdownMode::Graceful { deadline: Duration::from_secs(5) }).await;

Подробнее: Запуск, рестарт и остановка.

11. Чужие заявки не трогать

Правило. Заявки из списка foreign (поставлены руками, из приложения или другим роботом) не снимать и не менять. Показать человеку.

Почему. Бот не знает, зачем человек их поставил. Снятие может сломать чужую стратегию или защитный стоп.

Что сломается. Чужая позиция остается без защиты. Библиотека такие заявки сама не трогает и не прячет.

Как правильно.

for order in &report.foreign {
    // Placed by hand or by another program. No cancel, no replace. A person decides.
    println!("foreign order {}: {:?}", order.cid, order.state);
}

Подробнее: Круг жизни.

12. Длинную историю свечей читать окнами

Правило. Брокер ограничивает один запрос свечей по числу баров или длине периода (БКС: 1440 баров, Т-Банк: от дня для минутных до десяти лет для месячных). Историю длиннее читать через CandlePages или load_candles: они режут период на окна, ждут RateLimited, повторяют чтение после сбоя сети, склеивают окна без дублей и помнят, откуда продолжить.

Почему. Запрос длиннее предела брокер отклоняет. Ручная нарезка забывает про лимит запросов и про бары на краю окна.

Что сломается. Rejected на слишком длинном периоде, дубли баров на стыках окон, RateLimited посреди выгрузки без места, откуда продолжить.

Как правильно.

use brokapi::runtime::history::{CandlePages, HistoryOpts};
let mut pages = CandlePages::new(
    md,
    id,
    brokapi::core::Timeframe::H1,
    range,
    HistoryOpts::default(),
);
while let Some(bars) = pages.next_page().await? {
    // write `bars` to disk; after a stop continue from pages.resume_from()
    let _ = bars;
}

Повторяются только чтения. Команды не повторяются нигде в библиотеке.

Как вести себя после HistoryError: смотрите error.class(). Retryable (лимит, сеть): подождать (retry_after у RateLimited) и начать снова с resume_from или с места, где кончается файл, не больше нескольких попыток. Fatal (Rejected, Config, Unsupported): то же чтение упадет снова, бумагу записать в отчет и идти к следующей.

Подробнее: Как собрать исследовательский движок.

13. Unsupported - не сбой

Правило. Возможность, которой нет у брокера, возвращает Error::Unsupported { capability } сразу, без сети. Это не сбой: убрать вызов или проверить caps() заранее. Библиотека отсутствующий метод опросом не подменяет. Примеры: replace у Финама, свечи 10 минут у БКС, поток стоп-заявок у всех.

Почему. Подмена (отмена и новая заявка вместо replace) не атомарна: старая заявка может исполниться между шагами. Решение за ботом.

Что сломается. Бот, который считает Unsupported сбоем и повторяет, зациклится.

Подробнее: События и возможности.

Чего библиотека не делает

Это не недоработки. Это слой над коннекторами, его пишет бот:

ЧтоЧей слой
Стратегии, сигналы, управление рискомбот
Учет позиций, общий портфель по счетам, выбор счета для заявкибот
Журнал заявок на диске, восстановление состояния после рестартабот (библиотека дает cid в каждом событии и сверку)
Бэктест и подмена временинет. Для проверки кода есть имитации brokapi-mocks, для данных - запись и воспроизведение событий (исследовательский движок)
Автоповтор команднет и не будет
Снятие заявок при остановкебот
Работа с чужими заявкамичеловек
Хранение токеновбот (.env, в логи не попадают)

Частые ошибки агентов

Что агент обычно делает не так, когда пишет бота без чтения этой страницы, и как правильно.

ОшибкаЧто будетКак правильно
Включает запись (BCS_IS_WRITE=true) или берет токен с правом торговли, «чтобы проверить»Настоящая заявка на настоящий счет без ведома владельцаПравило 0: проверять на имитации и песочнице, на боевой счет только со слова человека
Ставит заявку в цикле повторов «пока не получится»Две заявки после первого Unknown. Для того же cid - duplicate_placeИсходы разные: Rejected - исправить причину и новый cid, RateLimited - подождать, Unknown - order_state. Правило 3
Считает Ok(OrderAck) исполнениемБот думает, что позиция открыта, а заявка только стоитOrderAck значит «брокер принял». Исполнение приходит событиями Event::Order и Event::Execution
Ставит заявку сразу после startNotReconciledПодписка, reconcile, потом торговля. Правило 2
Подписывается на статусы после place или после reconcileСобытия между снимком и подпиской потеряныПодписка раньше сверки и раньше первой заявки
Новый BrokerLimiter для каждого коннектораВместе превышают квоту брокераОдин на область квоты. Правило 4
Придумывает cid внутри повтора или использует cid из журнала повторноduplicate_placeНовая попытка - новый cid, старый cid остается в журнале как финальный
Использует f64 для ценыЦена не на шаге, округление уезжает на копейкуPrice, Decimal, round_to_step. Правило 8
Ждет внутри цикла чтения канала ответ на командуКанал переполняется, торговля закрывается (Degraded)Команды из другой задачи, чтение канала короткое. Правило 5
Хранит стакан и не смотрит на ResyncТорговля по устаревшему стакануВыбросить стакан. Правило 7
Не обрабатывает OverflowПосле него каждая команда - DegradedПравило 6
Считает сообщение Rejected кодомЛомается при смене формулировки брокераСравнивать code, не message
Считает not_found после Unknown окончательнымСтавит вторую заявку, пока первая в путиПодождать и спросить еще раз. Правило 3
Ждет, что shutdown снимет заявкиЗаявки остаются на биржеСнять самому до shutdown. Правило 10
Снимает все заявки счета «для чистоты»Снимает чужие защитные стопыТолько свои, из журнала. Правило 11
Отправляет период в год одним candlesRejected по длине периодаCandlePages. Правило 12
Не запоминает шаг ценыОтказ придет от брокера, а не до отправкиprice_grid().learn(&params)
Количество не кратно лоту, дробное или нулевоеinvalid_qty до отправкиКоличество в единицах инструмента, кратно лоту. Деньги, цены и количества
Берет идентификатор инструмента от другого брокераConfigfind у MD своего брокера
Подключает крейт без флага брокераНет brokapi::bcsfeatures = ["bcs"] и другие. Быстрый старт
Запускает #[tokio::main(flavor = "current_thread")] с блокирующим кодомЗависанияОбычный многопоточный tokio, блокирующее - в spawn_blocking
Пишет в журнал после placeПадение между вызовом и записью теряет заявкуДо вызова, с fsync. Правило 1
Ставит заявки через raw()Заявка не попадает ни в события, ни в reconcile, коннектор за ней не следитЗаявки только через OrderExecutor. raw

Перед запуском на настоящем счете

Прочитайте Перед боевым счетом и список допущений по документации брокеров: agents/unverified.md. Кроме них:

  1. Пройти тесты против имитаций (Как тестировать): путь Unknown, Overflow, рестарт со сверкой.
  2. Токены только в .env, в журнал и логи не попадают.
  3. Первая боевая заявка - только со слова человека, токен с правом торговли выдал он (правило 0).
  4. Известно, как остановить бота и что при этом останется на счете: Аварийная остановка бота.

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

brokapi by RunInfra