Сравниваем лучшие цены: этап 1 на пяти биржах

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

Урок 4 курса "Создай собственного арбитражного бота". Сегодня ваш бот начнет работать на реальных данных: мы запустим этап 1.

Что мы создадим

На уроке 1 мы сравнивали две биржи для одной монеты. Этап 1 настоящего бота сравнивает все пары бирж и множество монет, а затем показывает только разницы, которые остаются положительными после вычета комиссий. Сегодня возьмем пять бирж и одну монету и посмотрим, сколько таких разниц останется.

Три причины, по которым разница в цене может оказаться ненастоящей

Перед тем как перейти к коду, разберем три причины, по которым может показаться, что разница есть, хотя на самом деле ее нет.

Причина 1: комиссии. Каждая биржа берет процент от сделки. Комиссию за исполнение чужого предложения называют комиссией тейкера. Предположим, комиссия за покупку составляет 0.1 процента, а за продажу — 0.26 процента. Тогда для безубыточности разница должна быть больше 0.36 процента.

Причина 2: котируемая валюта. Цена указывается в какой-либо валюте. Для BTC/USDT цена выражена в привязанной к доллару монете USDT. Для BTC/USD — в настоящих долларах. Это разные цены. USDT может стоить немного больше или меньше доллара, а иногда разница бывает гораздо заметнее. Если сравнивать BTC/USDT на одной бирже с BTC/USD на другой без пересчета, разница может быть связана только с тем, что USDT и доллар стоят по-разному. Правило: сравнивайте цены в одной и той же котируемой валюте.

Причина 3: единицы измерения. Одна биржа может указывать цену за одну монету, другая — за тысячу, а некоторые указывают цену контракта с собственным размером. Всегда проверяйте, что означает одна единица.

Формула

Если покупать на бирже X, а продавать на бирже Y:

разница в процентах = (bid на Y − ask на X) / ask на X × 100

чистая разница = разница − комиссия на X − комиссия на Y

Если чистая разница больше нуля, пара проходит этап 1.

Пример. Ask на X равен 100.00, а комиссия на X — 0.10 процента. Bid на Y равен 100.50, а комиссия на Y — 0.10 процента. Разница = (100.50 − 100.00) / 100.00 × 100 = 0.50 процента. Чистая разница = 0.50 − 0.10 − 0.10 = 0.30 процента. Эта пара проходит проверку.

При тех же ценах, но с комиссией на Y в 0.40 процента: чистая разница = 0.50 − 0.10 − 0.40 = 0.00. Эта пара проверку не проходит.

Программа

Программа запрашивает у пяти бирж лучшие bid и ask для BTC/USDT, проверяет все пары бирж с учетом направления покупки и продажи и выводит пять лучших вариантов.

Это прямолинейный подход: два цикла, все сравниваются со всеми, один веб-запрос к каждой бирже при каждом запуске. Мы пишем его так намеренно. Это самая короткая программа, которая показывает проблему, и в следующем разделе вы почувствуете ее ограничения. Не запускайте такую схему в рабочем боте.

import json, urllib.request

def get(url):
    req = urllib.request.Request(url, headers={"User-Agent": "course-scanner"})
    with urllib.request.urlopen(req, timeout=10) as r:
        return json.load(r)

def binance(s):  d = get(f"https://api.binance.com/api/v3/ticker/bookTicker?symbol={s}USDT"); return float(d["bidPrice"]), float(d["askPrice"])
def kraken(s):   d = next(iter(get(f"https://api.kraken.com/0/public/Ticker?pair={s}USDT")["result"].values())); return float(d["b"][0]), float(d["a"][0])
def okx(s):      d = get(f"https://www.okx.com/api/v5/market/ticker?instId={s}-USDT")["data"][0]; return float(d["bidPx"]), float(d["askPx"])
def bitstamp(s): d = get(f"https://www.bitstamp.net/api/v2/ticker/{s.lower()}usdt/"); return float(d["bid"]), float(d["ask"])
def coinbase(s): d = get(f"https://api.exchange.coinbase.com/products/{s}-USDT/ticker"); return float(d["bid"]), float(d["ask"])

# имя: (функция, комиссия тейкера в процентах). Комиссии приведены для примера: проверьте страницу комиссий каждой биржи.
VENUES = {"binance": (binance, 0.10), "kraken": (kraken, 0.26), "okx": (okx, 0.10),
          "bitstamp": (bitstamp, 0.30), "coinbase": (coinbase, 0.40)}

def scan(symbol):
    quotes = {}
    for name, (fn, fee) in VENUES.items():
        try:
            quotes[name] = fn(symbol)
        except Exception as e:
            print("skip", name, "-", type(e).__name__)
    rows = []
    for buy, (_, ask) in quotes.items():
        for sell, (bid, _) in quotes.items():
            if buy == sell:
                continue
            gap = (bid - ask) / ask * 100
            fees = VENUES[buy][1] + VENUES[sell][1]
            rows.append((gap - fees, gap, buy, sell))
    rows.sort(reverse=True)
    for net, gap, buy, sell in rows[:5]:
        print(f"buy {buy:9} sell {sell:9} gap {gap:+.3f}%  after fees {net:+.3f}%")

scan("BTC")

Сохраните программу как scan5.py и запустите python3 scan5.py.

Какой результат вы должны увидеть

Пять строк. При пробном запуске программа вывела:

buy binance   sell okx       gap +0.004%  after fees -0.196%
buy okx       sell binance   gap -0.004%  after fees -0.204%
buy binance   sell kraken    gap +0.003%  after fees -0.357%

Ваши числа будут другими, но картина останется той же: разницы крошечные, всего несколько тысячных процента, а комиссии в сотни раз больше, поэтому чистая разница везде отрицательная. Это честный первый результат работы этапа 1 на крупной монете и крупных биржах. Если чистая разница окажется выше нуля, пока не радуйтесь. Проверьте три вещи: котируемую валюту, единицы измерения и свежесть котировок. Если биржа не обновляла данные целую минуту, цена будет старой.

Если биржа пропущена, вероятно, она блокирует пользователей из вашей страны. Удалите ее из VENUES или замените другой.

Почему так не стоит делать в рабочем боте

Прямой вариант работает для пяти бирж и одной монеты. Но посмотрим, что будет, когда бот вырастет.

  • Сравниваемые пары: при N биржах есть N × (N − 1) направленных пар. Для 5 бирж это 20, а для 30 — 870. И так для каждой монеты.
  • Запросы: при каждом запуске программа заново получает котировки — по одному запросу к каждой бирже для каждой монеты. Для 30 бирж и 200 монет это 6000 запросов на один проход. Биржи ограничивают число запросов и могут вас заблокировать, а 6000 запросов выполняются минуты, хотя разрыв может существовать всего секунды.
  • Устаревшие цены: пока приходит последний ответ, первая котировка уже устарела, и сравнение объединяет данные, полученные в разное время.

Коротко о том, как устроен рабочий бот:

  1. Он поддерживает постоянное подключение к каждой бирже через веб-сокет и получает каждое изменение цены сразу, поэтому ему не нужно регулярно опрашивать биржи.
  2. Он хранит в памяти лучшие bid и ask каждой биржи для каждой монеты.
  3. Для одной монеты ему достаточно найти самую высокую цену bid и самую низкую цену ask среди всех бирж: это один проход по N котировкам вместо N × (N − 1) сравнений. Если разница между лучшим bid и лучшим ask не покрывает комиссии даже для самой дешевой пары, проверять остальные незачем.

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

Где встречаются большие разницы

Разницы больше у небольших, редко торгуемых монет, на небольших биржах и во время резких движений цены. Они также увеличиваются, когда комиссии ниже. Многие биржи снижают комиссии тем, кто много торгует или держит собственный токен биржи. Профессиональный бот работает с комиссиями, которые вам в первый день недоступны. Это одна из причин, почему на очевидных парах так сложно конкурировать.

Попросите ИИ-агента написать и расширить программу

Расплывчатая просьба:

Сравни цены на многих биржах.

Просьба с нужными подробностями:

Доработай scan5.py. Добавь функцию, которая пропускает котировку старше 5 секунд, если биржа указывает время. Вынеси комиссии и список монет в config.py. Проверь BTC, ETH и SOL. Выводи только пары с чистой разницей выше 0, а если таких нет — три лучшие пары со словом NEAR. Не добавляй код для торговли. Покажи мне результат.

Затем проверьте вручную одну строку из результата: с помощью калькулятора вычислите (bid − ask) / ask × 100 по двум ценам, которые показывает программа, и сравните с колонкой разницы. Результат должен совпасть до третьего знака после запятой.

Практическое задание

  1. Запустите scan5.py пять раз с интервалом в минуту. Сколько раз чистая разница была выше нуля хотя бы у одной пары?
  2. Замените комиссии в VENUES на 0.02 процента для обеих сторон — это уровень очень крупного трейдера. Осталась ли теперь пара, проходящая проверку? Что это говорит о том, кто может заработать на такой разнице?
  3. Добавьте еще одну монету, например ETH. Лучшая разница для нее больше или меньше, чем для BTC?
  4. Сохраните копию файла как scan5_v1.py. Это ваша сохраненная версия для урока.

Что дальше

На уроке 4 мы сравнивали биржи. На уроке 5 изменим вопрос: а что, если разница находится не между двумя биржами, а в круговом обмене валют на одной бирже? Для этого цены нужно представить в виде графа.