Skip to content

Потоки стакана Binance Web Socket

Marcus Aurelius edited this page Dec 16, 2021 · 2 revisions

Потоки стакана - Binance Web Socket

Можно получить данные следующими способами:

  1. Только заявки на продажу и покупку, наиболее близкие к рыночной цене (All Book Tickers Stream, Individual Symbol Ticker Streams)

  2. Топ заявок на покупку и на продажу. Другими словами 20 заявок на продажу и 20 на покупку, которые ближе всего к рыночной цене. (можно получать топ из 5, 10 или 20 заявок) (Partial Book Depth Streams)

  3. Получать только обновления стакана, когда меняется объем - то есть когда заявка исполняется или снимается с исполнения. Полный стакан можно получить через GET запрос. (Diff. Depth Stream)

[TOC]

1. Individual Symbol Ticker Streams - Поток спреда стакана по одному тикеру

Название потока: <symbol>@bookTicker

Скорость обновления: Real-time

Получает информацию по лучшей цене покупки и продажи по определенному т. Лучшая цена - значит самая близкой к рыночной.

Скорость обновления Real-time - значит как только произошли изменения, веб-сокет их присылает. Как часто это происходит, зависит от тикерного симола - как часто у него меняется лучшее предложение. Например, ethbtc@bookTicker будет присылать изменения очень часто - ориентировочно 15 раз в секунду. А waxpbtc@bookTicker присылает 1-2 раза в секунду.

При обновлении приходят данные по ack и bid одновременно, даже если изменился только один из них. Изменения могут произойти как с ценой, так и с объемом.

img

Данные, которые присылает биржа:

{ поток bnbusdt@bookTicker
  "u": 400900217,     // id обновления стакана (не timestamp)
  "s": "BNBUSDT",     // тикерный символ
  "b": "25.35190000", // цена лучшей заявки на покупку (b - bid)
  "B": "31.21000000", // объем лучшей заявки на покупку 
  "a": "25.36520000", // цена лучшей заявки на продажу (a - ask)
  "A": "40.66000000"  // объем лучшей заявки на продажу
}

2. All Book Tickers Stream - Поток спреда стаканов по всем тикерам

Название потока: !bookTicker

Скорость обновления: Real-time

Получает информацию по лучшей цене покупки и продажи по всем тикерным символам. Лучшая цена - значит самая близкой к рыночной.

*Данные, которые присылает биржа (Json): *

Такие же данные, как у All Book Tickers Stream, но по разным тикерным символам(<symbol>@bookTicker)

{ // поток !bookTicker
  "u": 400900217,     // id обновления стакана (не timestamp)
  "s": "BNBUSDT",     // Тикерный символ (s - symbol)
  "b": "25.35190000", // цена лучшей заявки на покупку (b - bid)
  "B": "31.21000000", // объем лучшей заявки на покупку 
  "a": "25.36520000", // цена лучшей заявки на продажу (a - ask)
  "A": "40.66000000"  // объем лучшей заявки на продажу
}
{ // поток !bookTicker
"u": 1381280150,
"s": "SHIBUSDT"
"b": "0.00003822",
"B": "649709579.00",
"a": "0.00003823",
"a": "204601625.00"
}
{ // поток !bookTicker
"u": 54271051,
"s": "DEXEETH"
"b": "0.00571100",
"B": "11.96000000",
"a": "0.00578200",
"a": "289.59000000"
}

3. Partial Book Depth Streams - Поток части стакана

Название потока: <symbol>@depth<levels> или <symbol>@depth<levels>@100ms

Скорость обновления: 1000ms или 100ms

Поток отдает топ предложений на покупку и продажу (топ уровней цен стакана) - то, что обычно видно на Binance в стакане (скриншот стакана ETH/BTC на Binance ниже). Можно получать топ из 5, 10, или 20 значений (т.е. 20 на покупку и 20 на продажу).

<symbol> - это тикерный символ, например ethbtc или manausdt (обязательно в нижнем регистре). Если указан неправильный тикерный символ, то данные не будут приходить.

<levels> - это количество уровней стакана, можно указать 5, 10, или 20. Указывать их обязательно, иначе это будет уже другой поток (Поток обновлений стакана - Diff. Depth Stream)

@100ms или @1000ms - это время, как часто будут приходить данные. Можно не указывать, значение по умолчанию 1000ms

Примеры, как нужно указывать поток:

"ethbtc@depth5" <- получать данные стакана ETH/BTC по 5 уровням цен каждую секунду (1000ms по умолчанию) "ilvbusd@depth20"<- получать данные стакана ILV/BTC по 20 уровням цен каждую секунду (1000ms по умолчанию) "1inchbtc@depth20@100ms" <- получать данные стакана 1INCH/BTC по 20 уровням каждые 100 милисекунд

скриншот стакана на Binance

Web Socket присылает два массива, asks и bids.

Asks соответвуют заявкам выше середины на скриншоте (красные цены 🔴), это заявки на покупку.

Bids соответвуют заявкам ниже середины на скриншоте (зеленые цены 🟢), это заявки на продажу.

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

Данные, которые присылает биржа (Json):

{ // поток bnbbtc@depth5 
  "lastUpdateId": 160,  // ID последнего обновления стакана (не timestamp)
  "bids": [             // Массив из заявок на покупку - bids [цена, объем]
    [					
      "0.011706",       // Цена, которую обозначил покупатель
      "4.489"           // Объем покупки (количество монет)
    ],
    [
      "0.011707",         
      "3.804"              
    ],
    [
      "0.011708",         
      "0.67"              
    ],
    [
      "0.01171",         
      "14.245"              
    ],
    [
      "0.011711",         
      "11"              
    ]
  ],
  "asks": [             // массив из заявок на продажу - asks [цена, объем]
    [
      "0.011712",       // По какой цене продавец согласен продать монеты
      "11.941"          // Объем продажи (количество монет)
    ],
    [
      "0.011713",        
      "5.255"             
    ],
    [
      "0.011714",        
      "15"             
    ],
    [
      "0.011715",        
      "11.239             
    ],
    [
      "0.011717",        
      "0.416"             
    ]
  ]
}

4. Diff. Depth Stream - Поток обновлений стакана

Название потока: <symbol>@depth ИЛИ <symbol>@depth@100ms

Скорость обновления: 1000ms или 100ms

{ // Сообщение I 
  "e": "depthUpdate", 
  "E": 1638636116911, 
  "s": "BNBBTC",      
  "U": 2365900644,    // ID первого обновления в эвенте
  "u": 2365900673,    // ID последнего обновления в эвенте
  ...
}

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

Поток присылает только обновления, полный стакан он не присылает. Нужно либо получать полный стакан иначе. Если разорвется соединение, веб сокет не пришлет пропущенные обновления.

ID Обновления - U и u:

По поводу ID обновления, которое содержится в U и u подробной информации на Binance нет. Скорее всего, каждый ID привязан к изменению определенного уровня в стакане (исполнен ордер). Веб сокет присылает обновление раз в 1 секунду или в 100 милисекунд, в этом обновлении есть уровни, которые изменились за это время.

  • U - ID первого обновления в эвенте
  • u - ID последнего обновления в эвенте

С помощью ID можно понять, уже учтено это обновление, или ещё нет. Для этого нужно сравить u двух обновлений - нового обновления, и предыдущего. Если u предыдущего больше, то это обновление не актуально, его нужно проигнорировать.

Пропущенные обновления

Также с помощью ID можно определить, были ли пропущены обновления. Для этого нужно сравнить u предыдущего c U нового обновления.

Пример, когда обновления были пропущены (Json обрезан, для краткости):

{ // Сообщение I 
  "e": "depthUpdate", 
  "E": 1638636116911, 
  "s": "BNBBTC",      
  "U": 2365900644,    // ID первого обновления в эвенте
  "u": 2365900673,    // ID последнего обновления в эвенте
  ...
}
       
// произошел разрыв соединения, данные не получены
        
{ // Сообщение II
  "e": "depthUpdate", 
  "E": 1638636118910, 
  "s": "BNBBTC",      
  "U": 2365900682,    // ID первого обновления в эвенте
  "u": 2365900688,    // ID последнего обновления в эвенте
  ...
}
{ // Сообщение III
  "e": "depthUpdate", 
  "E": 1638636119910, 
  "s": "BNBBTC",      
  "U": 2365900689,    // ID первого обновления в эвенте
  "u": 2365900722,    // ID последнего обновления в эвенте
  ...
}      

В примере последнее обновление в Сообщение I u и первое обновление в Сообщение II U отличаются больше, чем на 1. Значит, между ними были пропущены данные (из-за разрыва соединения). А вот Сообщение II и Сообщение III последовательны (U третьего больше u второго на 1).

Получение полного стакана

Чтобы получить полный стакан, нужно отправить GET запрос к этому эндпоинту: api.binance.com/api/v3/depth

Данные, которые присылает биржа (Json):

{ // поток bnbbtc@depth 
  "e": "depthUpdate", // Тип эвента
  "E": 1638631942729, // Время обновления (Unix TimeStamp)
  "s": "BNBBTC",      // Тикерный символ
  "U": 2365796799,    // ID первого обновления в эвенте
  "u": 2365796820,    // ID последнего обновления в эвенте
  "b": [              // Массив из заявок на покупку - bids [цена, объем]
    [					
      "0.01168500",   // Цена, которую обозначил покупатель
      "6.28800000"    // Объем покупки (количество монет)
    ],
    [
      "0.01162100",         
      "11.71800000"              
    ]
  ],
  "a": [              // Массив из обновившихся заявок на продажу - asks [цена, объем]
    [
      "0.01170700",   // По какой цене продавец согласен продать монеты
      "22.4870000"    // Объем продажи (количество монет)
    ],
    [
      "0.01170500",        
      "22.65900000"             
    ],
    [
      "0.01170400",        
      "26.30400000"             
    ]
  ]
}

Как вести локальный стакан

Задача

Получение полного стакана BNB/BTC, сохранение его локально, и максимально быстрая синхронизация с Binance.

Алгоритм решения
  1. Чтобы получать обновления, подключаюсь к веб сокету wss://stream.binance.com:943/ws/bnbbtc@depth. Пока не получу полный стакан (шаг 2, это следующий шаг), полученные данные не обрабатываю.

  2. Получаю стакан из эндпоинта https://api.binance.com/api/v3/depth?symbol=BNBBTC&limit=1000. Это полный стакан, с глубиной в 1000. Также ответ от Binance содержит lastUpdateId, сохраняю его, чтобы потом сравнивать обновления с ним. Это id последнего обновления, которое записано в локальный стакан - в случае с полным стаканом, это id последнего обновления, которое Binance записал в стакан.

{
  "lastUpdateId": 12, // сохраняю этот ID, он нужен, чтобы проверять обновления
  "bids": [
    [
      "0.01163500",
      "16.36800000"
    ],
    [
      "0.01163400",
      "6.26900000"
    ],
    // ...ещё 998 уровней цен бидов
  ],
  "asks": [
    [
      "0.01163800",
      "12.34500000"
    ],
    [
      "0.01163900",
      "20.57200000"
    ],
   	... // ещё 998 уровней цен асков
  ]
}
  1. Начинаю обрабатывать обновления, которые получаю из потока /ws/bnbbtc@depth, подключенного на шаге 1.
{ // поток bnbbtc@depth 
  "e": "depthUpdate", // Тип эвента
  "E": 1638631942729, // Время обновления (Unix TimeStamp)
  "s": "BNBBTC",      // Тикерный символ 
  "U": 10,    		  // ID первого обновления в эвенте
  "u": 15,    		  // ID последнего обновления в эвенте
  "b": [              // Массив из заявок на покупку - bids [цена, объем]
    [					
      "0.01168500",   // уровень цены
      "6.28800000"    // новый объем по этому уровню цены
    ],
    ...// сокращено: дальше следующие обновления бидов
  ],
  "a": [              // Массив из обновившихся заявок на продажу - asks [цена, объем]
    [
      "0.01170700",   
      "22.4870000" 
    ],
    ...// сокращено: дальше следующие обновления асков
}
  1. Пропускаю обновления, где u <= lastUpdateId. Если это условия не соблюдается, значит пришло устаревшее обновление (локальный стакана синхронизирован с более новым обновлением, чем мы получили).

  2. Первое обработанное обновление должно соответствовать условиям:

    • U <= lastUpdateId+1

    • u >= lastUpdateId+1

Эти условия нужны, т.к. /ws/bnbbtc@depth отдает обновления пачками - те обновления, которые произошли за секунду. А вот эндпоинт https://api.binance.com/api/v3/depth?symbol=BNBBTC&limit=1000 отдает данные, которые актуальны в данный момент - т.е. в любой момент, не обязательно кратный секунде. Поэтому lastUpdateId (последнее обновление эндпоинта) и u (последнее обновление, присланное веб сокетом) могут отличаться.

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

  1. Пока я подключен к потоку, у каждого нового обновления U должен быть равен u+1 предыдущего обновления. Если U нового обновления > u+1 предыдущего обновления, то я пропустил обновления. Мне нужно перейти к шагу 2, чтобы получить полный стакан.

  2. В переменной lastUpdateId находится id последнего обновления, которое загружено в локальный стакан. После того, как я записываю обновление в локальный стакан, я присваиваю lastUpdateId значение u обновления - то есть id последнего обновления, с которым синхронизирован локальный стакан.

  3. Каждое обновление содержит абсолютный объем для каждого уровня цен. Значит мне нужно перезаписывать объем для каждого уровня цен. Нужно отметить, что данные по уровням цен приходят отсортированными: a(asks) по возрастанию цены (10, 11, 15), b(bids) по убыванию (9, 6, 5). Но удобнее хранить уровни цен локально в виде словаря (map, dictionary, в зависимости от языка программирования).

{ // пример сортировки уровней цен
    "e": "depthUpdate", // Тип эвента
    "E": 1638723561756, // Время обновления (Unix TimeStamp)
    "s": "ATMUSDT",      
    "U": 157079052,    	// ID первого обновления в эвенте
    "u": 157079091,		// ID последнего обновления в эвенте
    "b": [ // сортировка бидов по возрастанию
        [
            "9.05000000",
            "0.00000000"
        ],
        [
            "9.06000000",
            "104.53000000"
        ],
        [
            "9.16000000",
            "51.45000000"
        ],
        [
            "9.20000000",
            "16.51000000"
        ]
    ]
    "a": [ // сортировака асков по убыванию
        [
            "9.04000000",
            "0.00000000"
        ],
        [
            "9.03000000",
            "3.04000000"
        ],
        [
            "9.02000000",
            "44.55000000"
        ],
        [
            "9.01000000",
            "0.00000000"
        ],
        [
            "8.98000000",
            "0.00000000"
        ]
    ]
}
  1. Если объем равен 0, то я удаляю уровень цены.
{  
    "e": "depthUpdate", // Тип эвента
    "E": 1638723330746, // Время обновления (Unix TimeStamp)
    "s": "ATMUSDT",      
    "U": 157077424,    	// ID первого обновления в эвенте
    "u": 157077442, 	// ID последнего обновления в эвенте
    ...
    "b":
        ["8.86000000",	"93.35000000"],
        ["8.83000000",	"2967.97000000"]
    ],
    "a": [
        ["8.96000000",	"1094.87000000"],
        ["8.98000000",	"0.00000000"], 	 // объем равен нулю, нужно удалить 8.98 из локального стакана
        ["9.00000000",	"384.96000000"],
        ["9.02000000",	'0.00000000'] 	 // объем равен нулю, нужно удалить 9.02 из локального стакана
    ]
}
Схема получения обновлений

Вот схема получения обновлений - как lastUpdateId соотносится с uи U (схема соответствуют примерам Json выше):

схема получения обновлений