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

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

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

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

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

Деньги, цены и количества

Цена, количество, лот, баланс и комиссия в библиотеке - точные десятичные числа. f64 для них нигде не используется. Страница объясняет, почему так, как создавать числа, как округлять цену до шага и что делает библиотека при переводе форматов брокеров.

Python. Цена, количество и деньги - decimal.Decimal. В вызов можно передать Decimal, int или строку, float отвергается с TypeError. Округление до шага - brokapi.round_to_step(цена, шаг, RoundMode.DOWN), правила те же, что у round_to_step в Rust. Время - datetime с часовым поясом.

Три типа

ТипЧто этоГде встречается
Priceцена одной бумагизаявки, стакан, сделки, свечи, stop_price, price_step
Qtyколичество в единицах инструмента, размер лотазаявки, стакан, сделки, исполнения, позиции, объем свечи, lot_size, min_qty
Moneyсумма денегбаланс (available, blocked), комиссия, price_step_value

Внутри у каждого Decimal (крейт rust_decimal): целое на 96 бит и до 28 знаков после запятой. Типы разные нарочно: количество нельзя подставить вместо цены, а сумму в рублях вместо количества. Библиотека отдает Decimal и макрос dec! из brokapi_core.

Почему не f64

Брокеры присылают цены десятичными числами, и сетка биржи тоже десятичная. В двоичной дроби 244.3 не лежит ровно:

assert_ne!(0.1_f64 + 0.2, 0.3);          // 0.30000000000000004
assert_ne!(244.3_f64 % 0.01, 0.0);       // остаток 6.3e-15: цена «не на шаге»
assert_ne!((109.32_f64 / 0.01).fract(), 0.0); // 10931.999999999998 шагов

Для заявки это значит: цена, которая на бирже верная, у вас «не кратна шагу», а округление до шага уезжает на одну копейку. Суммы портфеля набегают копейками. С Decimal те же числа точные: 0.1 + 0.2 == 0.3, 244.3 кратно 0.01.

Эталон, NautilusTrader, хранит цену, количество и деньги целыми числами с фиксированным масштабом. Мы взяли другое: Decimal со своим масштабом у каждого числа. Брокеры дают разное число знаков (Т-Банк до девяти, БКС до восьми у цены, Финам строку любой длины), и один общий масштаб либо режет цену, либо сужает диапазон. Скорость целых нам не нужна: это слой подключения, а не движок, между числом и биржей сетевая задержка в миллисекунды.

Как создать число

use brokapi::core::{Decimal, Price, Qty, dec};

let a = Price::new(dec!(250.15));            // из литерала
let b: Price = "250.15".parse()?;            // из текста: файл, форма, переменная окружения
let c = Price::from(250);                    // из целого
let d = Price::from_scaled(25015, 2);        // из мантиссы и числа знаков: 250.15
assert_eq!(a, b);

let lots = Qty::from(10);
let sum: Qty = [Qty::from(1), Qty::from(2)].into_iter().sum();
  • Запятая не разделитель: "12,5" - ошибка разбора (NumberError::Parse).
  • Числа сравниваются по значению: Price 244.30 равно 244.3. А текст сохраняет запись: 244.30 печатается 244.30.
  • Price * Decimal дает Price (так считается «на 1% ниже»: bid * Decimal::new(99, 2)). Масштаб произведения больше: 244.30 * 0.99 печатается 241.8570. После round_to_step до шага 0.01 значение 241.85, а печать может быть 241.8500: значение то же, масштаб записи не важен.
  • Из f64 число делается только явно и с округлением до заданного числа знаков: Price::from_f64_lossy(244.3, 2). Имя говорит, что точность потеряна: f64 уже принес двоичный шум, округление его только прячет. Обратно - to_f64_lossy(), для графика или строки лога, но не для цены заявки.
  • В JSON число - строка: "244.30". Так его не округлит ни один читатель.

Как округлить до шага цены

Шаг цены и размер лота лежат в TradingParams (Справочник API). Округление точное, сетка начинается с нуля:

use brokapi::core::{Price, RoundMode, dec};

let step: Price = "0.05".parse()?;
let p: Price = "101.03".parse()?;
assert_eq!(p.round_to_step(step, RoundMode::Down)?, "101.00".parse()?);     // покупка: не дороже расчета
assert_eq!(p.round_to_step(step, RoundMode::Up)?, "101.05".parse()?);       // продажа: не дешевле расчета
assert_eq!(p.round_to_step(step, RoundMode::Nearest)?, "101.05".parse()?);  // ближайшая, половина - от нуля
assert!(p.round_to_step(step, RoundMode::Up)?.is_on_step(step));

Если параметры инструмента уже прочитаны (trading_params), короче так: params.round_price(price, mode) и params.round_qty(qty, mode). Нулевой или отрицательный шаг - ошибка NumberError::StepNotPositive.

Проверка цены коннектором

Коннектор не знает шаг сам: он узнает его от вас. У каждого коннектора заявок есть price_grid(). Заполните его один раз на инструмент:

let params = md.trading_params(&sber).await?;
ud.price_grid().learn(&params);

Дальше place, replace и place_stop проверяют цену заявки (и stop_price, limit_price). Цена не на шаге - Rejected с кодом price_off_step, до отправки: квота не тратится, cid остается свободным. Если шаг инструменту не передан, цена не проверяется, решает брокер. Таблицу можно задать и при создании: поле price_grid конфига UserDataConfig, одну на несколько коннекторов (копии таблицы - один и тот же список).

Количество проверяется так же и по той же таблице: price_grid().learn(&params) запоминает и шаг цены, и размер лота. Количество, которое не является целым числом лотов, это Rejected с кодом invalid_qty, до отправки. Если размер лота инструменту не передан, количество с лотом не сверяется, решает брокер (у Т-Банка лот коннектор читает сам, см. ниже). Нуль, отрицательное и дробное количество отказывают всегда, где брокер берет только целые. Ничего не округляется: округленная заявка купила бы не то, что вы просили.

Количество: единицы инструмента, не лоты

Qty везде значит число единиц инструмента: штук акций, облигаций, единиц валюты, контрактов. Одно и то же в заявке, в уровне стакана, в сделке рынка, в исполнении, в позиции и в объеме свечи, у любого брокера. Так же считает NautilusTrader и сама биржа. Брокер, который считает лотами (Т-Банк), переводится коннектором в обе стороны: ваш код лотов не видит.

Что такое лот и единица на разных рынках:

РынокЕдиница (Qty = 1)Лот (TradingParams::lot_size)Пример
Акцииодна акциячисло акций в лоте на бирже: 1, 10, 100у SBER лот 10: заявка на 30 это три лота
Облигацииодна облигацияобычно 1цена в процентах от номинала, а не в рублях: Price 98,5 значит 98,5% номинала. Сколько это в деньгах, считает бот: номинал, процент и накопленный купон
Валюта на биржеодна единица валютыобычно 1000у пары с лотом 1000 заявка на 3000 это три лота
Фьючерсы и опционыодин контракт1цена в пунктах, стоимость шага в деньгах - price_step_value. Сколько базового актива в контракте, библиотека не передает

Правила, одинаковые для всех:

  • Количество заявки положительное и кратное лоту. Иначе Rejected с кодом invalid_qty, ничего не отправлено, cid не потрачен.
  • Лот узнается из TradingParams::lot_size (в единицах), минимальное количество min_qty равно одному лоту.
  • Позиция, сделка и объем свечи приходят в единицах так, как их присылает брокер, коннектор пересчитывает только то, что брокер считает лотами.

Что делает каждый коннектор:

БрокерЧто считает APIЧто делает коннектор
БКСorderQuantity в штуках (документация: «Количество в заявке (шт.)»), целое не меньше 1передает число как есть. Дробное и нуль: invalid_qty. Кратность лоту проверяется, если лот передан в price_grid()
Т-Банкзаявки, стакан, сделки рынка и свечи в лотах, свои сделки и позиции в штукахузнает лот инструмента один раз (GetInstrumentBy, запрос к квоте) и переводит: заявку делит на лот, стакан, сделки рынка, свечи и статусы заявок умножает. Не кратно лоту: invalid_qty до отправки
Финамштуки, число десятичноепередает как есть. Кратность лоту проверяется, если лот передан в price_grid()
АЛОРзаявка в лотах, ответы заявок, сделок и позиций и в лотах, и в штуках, а уровни стакана, лента и свечи - как пришло (единицу объема в них документация не называет); объем лучших цен в котировках - в лотахчитает размер лота один раз (lotsize справочника), делит заявку на лот и читает штуки из ответов. Некратное лоту: invalid_qty, ничего не округляется. Объем котировки умножает на лот
Bybitколичество в единицах базовой монеты, десятичное, у каждого контракта свой шаг (qtyStep) и минимум (minOrderQty)читает правила контракта один раз и до отправки отказывает: не кратное шагу или меньше минимума invalid_qty, цена не на шаге price_off_step. Минимальную стоимость заявки и предельное количество не проверяет: ответит биржа

Как числа переводятся из формата брокера

Ни один путь не идет через f64. Что делает каждый коннектор:

БрокерФорматВ библиотекуИз библиотеки
Т-БанкQuotation и MoneyValue: целая часть units и миллиардные доли nano, у обеих частей знак числаunits + nano / 10^9, точнона части: до 9 знаков, иначе Rejected { invalid_price }, молчаливого округления нет
Финамgoogle.type.Decimal (строка, допускает знак и порядок) и google.type.Money (units, nanos)строка разбирается как естьстрока из числа без порядка, запись сохраняется: 244.30 уходит как "244.30"
БКСчисла JSONсразу в Decimal из текста ответа (serde_json с arbitrary_precision)JSON-числом с теми же знаками
Bybitчисла в строках JSON ("0.073547")сразу в Decimal из текста ответастрокой с теми же знаками

Что именно сохраняется: 244.30 не теряет нуль на конце, 0.0001 остается 0.0001, число с 19 знаками не обрезается. Это проверено тестами на всех форматах, в том числе случайными числами туда и обратно.

Проценты в моделях БКС (доля в портфеле, доходность, изменение в процентах, ставка купона) - это отношения, а не деньги, цены или количества: поэтому они f64. Деньги и цены в brokapi-bcs-protocol - Decimal.

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

brokapi by RunInfra