Как читать и обходить стакан заявок

Урок 9 курса по арбитражу. Что такое стакан заявок, как читать bid и ask с объемами, как обходить уровни, чтобы узнать реальную цену сделки, и короткая функция на Python для проверки вручную.

Урок 9 курса "Собери своего арбитражного бота". Здесь начинается мир 3: этап 2 бота, симуляция исполнения. До сих пор мы использовали одну цену для каждой стороны. Сегодня вы узнаете, почему одной цены недостаточно.

Торговая лавка

Представьте торговую лавку, где продают яблоки. У продавца есть несколько ящиков, и у каждого своя цена. В первом ящике 2 кг по 100 монет за килограмм. Во втором - 3 кг по 100.20 за килограмм. Если вам нужно 2 кг, вы платите по 100 за килограмм. Если нужно 5 кг, первые 2 кг стоят по 100, а следующие 3 кг - по 100.20. Средняя цена будет выше первой.

Стакан заявок - такая же лавка, только для монеты. В нем две стороны.

  • Сторона ask содержит заявки продавцов. В каждой строке указано: "Продаю такой-то объем по такой-то цене". Строки идут от самой низкой цены (лучший ask) вверх. Когда вы покупаете, вы выбираете эти строки сверху.
  • Сторона bid содержит заявки покупателей. В каждой строке указано: "Куплю такой-то объем по такой-то цене". Строки идут от самой высокой цены (лучший bid) вниз. Когда вы продаете, вы выбираете эти строки сверху.

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

Небольшой стакан:

Цена askОбъем ask
100.203
100.002
Цена bidОбъем bid
99.905
99.804

(Здесь ask записаны от худшей цены к лучшей, как их показывают многие биржи, поэтому лучший ask и лучший bid стоят рядом посередине.)

Лучший ask - 100.00, лучший bid - 99.90, спред - 0.10.

Прямой способ: одна цена умножить на объем

Простой способ - умножить лучшую цену на объем: 3 монеты по 100.00 стоят 300.00. Именно это неявно предполагал сканер из уроков 1-4. На деле стоимость ниже равна 300.20, поэтому простой расчет ошибается на 0.20 при сделке на 3 монеты, и ошибка растет вместе с объемом. При арбитражной марже 0.10 эта разница решает, будет прибыль или убыток. Используйте простой расчет только для быстрой предварительной оценки, но не для решения.

Обход стакана

Чтобы узнать стоимость сделки, начните с лучшего уровня, возьмите весь его объем (или столько, сколько вам еще нужно), затем перейдите к следующему уровню. Продолжайте, пока не наберете нужный объем.

Пример. Вы покупаете 3 монеты в стакане выше (на стороне ask).

  • Уровень 1: 100.00, объем 2. Берем 2 монеты. Стоимость 2 × 100.00 = 200.00. Осталась 1.
  • Уровень 2: 100.20, объем 3. Берем 1 монету. Стоимость 1 × 100.20 = 100.20.
  • Итого: 300.20. Средняя цена 300.20 / 3 = 100.0667.

Вы рассчитывали торговать по 100.00, а средняя цена вышла 100.0667. Дополнительные 0.0667 на монету называются проскальзыванием. Оно растет вместе с объемом сделки.

Продажа работает так же на стороне bid. Если продать 3 монеты в заявки на покупку: 1 монету по 100.30 и 5 по 99.90, получится 1 × 100.30 + 2 × 99.90 = 300.10. Средняя цена - 100.0333.

Арбитраж, который исчез

В уроке 3 на экране были ask 100.00 на бирже A и bid 100.30 на бирже B, разница 0.30. Обойдем оба стакана для 3 монет: на A заплатим 300.20, на B получим 300.10. Итого −0.10. Разницы, о которой сообщил этап 1, для сделки на 3 монеты нет. Она есть для 1 монеты, потому что верхний уровень B содержит 1 монету: купить 1 монету на A стоит 100.00, а продать 1 монету на B можно за 100.30, то есть прибыль 0.30. Значит, разница была реальной для 1 монеты, но не для 3.

Поэтому на этапе 2 вопрос звучит не "есть ли разница?", а "при каком объеме прибыль положительна и какой объем самый выгодный?"

Код

def walk(levels, amount):
    """levels: [(price, size), ...], best price first.
    Returns (money, filled): money spent or received, and how much was actually filled."""
    money, left = 0.0, amount
    for price, size in levels:
        take = min(size, left)
        money += take * price
        left -= take
        if left <= 0:
            break
    return money, amount - left

asks_A = [(100.00, 2), (100.20, 3)]      # здесь вы покупаете
bids_B = [(100.30, 1), (99.90, 5)]       # здесь вы продаете

paid, got_a = walk(asks_A, 3)
received, got_b = walk(bids_B, 3)
print("paid", paid, "for", got_a, "coins")
print("received", received, "for", got_b, "coins")
print("net", round(received - paid, 2))
print("average buy price", round(paid / got_a, 4), "average sell price", round(received / got_b, 4))
print(walk(asks_A, 10))

Результат:

paid 300.2 for 3 coins
received 300.1 for 3 coins
net -0.1
average buy price 100.0667 average sell price 100.0333
(500.6, 5)

Последняя строка показывает, что происходит, когда в стакане мало объема: вы запросили 10 монет, а в стакане всего 5, поэтому получили (500.6, 5): 5 монет за 500.60. Всегда проверяйте, что filled равен нужному объему.

Настоящий стакан

Можно скачать настоящий стакан. Этот код читает пять верхних уровней Bitcoin на публичной бирже. Аккаунт или ключ не нужны:

import json, urllib.request
req = urllib.request.Request("https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5",
                             headers={"User-Agent": "course"})
d = json.load(urllib.request.urlopen(req, timeout=10))
asks = [(float(p), float(q)) for p, q in d["asks"]]
bids = [(float(p), float(q)) for p, q in d["bids"]]
print("asks", asks[:3])
print("bids", bids[:3])

В одном запуске первые ask были (75694.8, 4.63912), а первые bid - (75694.79, 3.97826). Обратите внимание на два факта. Спред равен одному центу, это очень мало. На верхнем уровне около 4 монет, а на следующих уровнях объемы гораздо меньше одной монеты. Сделка на 10 монет дошла бы до уровней с менее выгодными ценами. У вас числа будут другими.

Два важных предупреждения

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

Попросите ИИ-агента написать и проверить код

Расплывчатый запрос:

Добавь поддержку стакана заявок.

Запрос со встроенной проверкой:

Напиши book.py с функцией walk(levels, amount), которая обходит уровни от лучшей цены и возвращает (money, filled). Проверка 1: для asks [(100.00, 2), (100.20, 3)] и объема 3 результат должен быть (300.2, 3). Проверка 2: для bids [(100.30, 1), (99.90, 5)] и объема 3 результат должен быть (300.1, 3). Проверка 3: для ask и объема 10 результат должен быть (500.6, 5). Затем добавь функцию, которая скачивает первые 20 уровней из открытого метода depth API Binance и обходит их для объемов 0.1, 1 и 10 BTC, печатая среднюю цену каждого. Ничего не покупай и не продавай.

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

Ваше упражнение

  1. Запустите код и проверьте, что первый результат в точности совпадает с приведенным выше.
  2. Вручную составьте стакан с тремя ask-уровнями на свой выбор. Посчитайте стоимость покупки объема, который задействует все три уровня, и среднюю цену. Затем проверьте расчет функцией.
  3. Запустите функцию для настоящего стакана с объемами 0.1, 1 и 10 BTC. Насколько растет средняя цена с увеличением объема?
  4. Сохраните код как новую версию.

Что дальше

Теперь вы умеете считать настоящую стоимость сделки по одному стакану. У круга несколько этапов, у каждого свой стакан и комиссия. В уроке 10 мы объединим их и найдем объем, при котором прибыль максимальна.