ccxt
Version:
943 lines (940 loc) • 218 kB
JavaScript
// ----------------------------------------------------------------------------
// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
// EDIT THE CORRESPONDENT .ts FILE INSTEAD
// ----------------------------------------------------------------------------
import binanceRest from '../binance.js';
import { Precise } from '../base/Precise.js';
import { ChecksumError, ArgumentsRequired, BadRequest, NotSupported } from '../base/errors.js';
import { ArrayCache, ArrayCacheByTimestamp, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide } from '../base/ws/Cache.js';
import { sha256 } from '../static_dependencies/noble-hashes/sha256.js';
import { rsa } from '../base/functions/rsa.js';
import { eddsa } from '../base/functions/crypto.js';
import { ed25519 } from '../static_dependencies/noble-curves/ed25519.js';
// -----------------------------------------------------------------------------
export default class binance extends binanceRest {
describe() {
const superDescribe = super.describe();
return this.deepExtend(superDescribe, this.describeData());
}
describeData() {
return {
'has': {
'ws': true,
'watchBalance': true,
'watchLiquidations': true,
'watchLiquidationsForSymbols': true,
'watchMyLiquidations': true,
'watchMyLiquidationsForSymbols': true,
'watchBidsAsks': true,
'watchMyTrades': true,
'watchOHLCV': true,
'watchOHLCVForSymbols': true,
'watchOrderBook': true,
'watchOrderBookForSymbols': true,
'watchOrders': true,
'watchOrdersForSymbols': true,
'watchPositions': true,
'watchTicker': true,
'watchTickers': true,
'watchMarkPrices': true,
'watchMarkPrice': true,
'watchTrades': true,
'watchTradesForSymbols': true,
'createOrderWs': true,
'editOrderWs': true,
'cancelOrderWs': true,
'cancelOrdersWs': false,
'cancelAllOrdersWs': true,
'fetchBalanceWs': true,
'fetchDepositsWs': false,
'fetchMarketsWs': false,
'fetchMyTradesWs': true,
'fetchOHLCVWs': true,
'fetchOrderBookWs': true,
'fetchOpenOrdersWs': true,
'fetchOrderWs': true,
'fetchOrdersWs': true,
'fetchPositionWs': true,
'fetchPositionForSymbolWs': true,
'fetchPositionsWs': true,
'fetchTickerWs': true,
'fetchTradesWs': true,
'fetchTradingFeesWs': false,
'fetchWithdrawalsWs': false,
},
'urls': {
'test': {
'ws': {
'spot': 'wss://stream.testnet.binance.vision/ws',
'margin': 'wss://stream.testnet.binance.vision/ws',
'future': 'wss://fstream.binancefuture.com/ws',
'delivery': 'wss://dstream.binancefuture.com/ws',
'ws-api': {
'spot': 'wss://ws-api.testnet.binance.vision/ws-api/v3',
'future': 'wss://testnet.binancefuture.com/ws-fapi/v1',
'delivery': 'wss://testnet.binancefuture.com/ws-dapi/v1',
},
},
},
'api': {
'ws': {
'spot': 'wss://stream.binance.com:9443/ws',
'margin': 'wss://stream.binance.com:9443/ws',
'future': 'wss://fstream.binance.com/ws',
'delivery': 'wss://dstream.binance.com/ws',
'ws-api': {
'spot': 'wss://ws-api.binance.com:443/ws-api/v3',
'future': 'wss://ws-fapi.binance.com/ws-fapi/v1',
'delivery': 'wss://ws-dapi.binance.com/ws-dapi/v1',
},
'papi': 'wss://fstream.binance.com/pm/ws',
},
},
'doc': 'https://developers.binance.com/en',
},
'streaming': {
'keepAlive': 180000,
},
'options': {
'returnRateLimits': false,
'streamLimits': {
'spot': 50,
'margin': 50,
'future': 50,
'delivery': 50, // max 200
},
'subscriptionLimitByStream': {
'spot': 200,
'margin': 200,
'future': 200,
'delivery': 200,
},
'streamBySubscriptionsHash': this.createSafeDictionary(),
'streamIndex': -1,
// get updates every 1000ms or 100ms
// or every 0ms in real-time for futures
'watchOrderBookRate': 100,
'liquidationsLimit': 1000,
'myLiquidationsLimit': 1000,
'tradesLimit': 1000,
'ordersLimit': 1000,
'OHLCVLimit': 1000,
'requestId': this.createSafeDictionary(),
'watchOrderBookLimit': 1000,
'watchTrades': {
'name': 'trade', // 'trade' or 'aggTrade'
},
'watchTicker': {
'name': 'ticker', // ticker or miniTicker or ticker_<window_size>
},
'watchTickers': {
'name': 'ticker', // ticker or miniTicker or ticker_<window_size>
},
'watchOHLCV': {
'name': 'kline', // or indexPriceKline or markPriceKline (coin-m futures)
},
'watchOrderBook': {
'maxRetries': 3,
'checksum': true,
},
'watchBalance': {
'fetchBalanceSnapshot': false,
'awaitBalanceSnapshot': true, // whether to wait for the balance snapshot before providing updates
},
'watchLiquidationsForSymbols': {
'defaultType': 'swap',
},
'watchPositions': {
'fetchPositionsSnapshot': true,
'awaitPositionsSnapshot': true, // whether to wait for the positions snapshot before providing updates
},
'wallet': 'wb',
'listenKeyRefreshRate': 1200000,
'ws': {
'cost': 5,
},
'tickerChannelsMap': {
'24hrTicker': 'ticker',
'24hrMiniTicker': 'miniTicker',
'markPriceUpdate': 'markPrice',
// rolling window tickers
'1hTicker': 'ticker_1h',
'4hTicker': 'ticker_4h',
'1dTicker': 'ticker_1d',
'bookTicker': 'bookTicker',
},
},
};
}
requestId(url) {
const options = this.safeDict(this.options, 'requestId', this.createSafeDictionary());
const previousValue = this.safeInteger(options, url, 0);
const newValue = this.sum(previousValue, 1);
this.options['requestId'][url] = newValue;
return newValue;
}
stream(type, subscriptionHash, numSubscriptions = 1) {
const streamBySubscriptionsHash = this.safeDict(this.options, 'streamBySubscriptionsHash', this.createSafeDictionary());
let stream = this.safeString(streamBySubscriptionsHash, subscriptionHash);
if (stream === undefined) {
let streamIndex = this.safeInteger(this.options, 'streamIndex', -1);
const streamLimits = this.safeValue(this.options, 'streamLimits');
const streamLimit = this.safeInteger(streamLimits, type);
streamIndex = streamIndex + 1;
const normalizedIndex = streamIndex % streamLimit;
this.options['streamIndex'] = streamIndex;
stream = this.numberToString(normalizedIndex);
this.options['streamBySubscriptionsHash'][subscriptionHash] = stream;
const subscriptionsByStreams = this.safeValue(this.options, 'numSubscriptionsByStream');
if (subscriptionsByStreams === undefined) {
this.options['numSubscriptionsByStream'] = this.createSafeDictionary();
}
const subscriptionsByStream = this.safeInteger(this.options['numSubscriptionsByStream'], stream, 0);
const newNumSubscriptions = subscriptionsByStream + numSubscriptions;
const subscriptionLimitByStream = this.safeInteger(this.options['subscriptionLimitByStream'], type, 200);
if (newNumSubscriptions > subscriptionLimitByStream) {
throw new BadRequest(this.id + ' reached the limit of subscriptions by stream. Increase the number of streams, or increase the stream limit or subscription limit by stream if the exchange allows.');
}
this.options['numSubscriptionsByStream'][stream] = subscriptionsByStream + numSubscriptions;
}
return stream;
}
/**
* @method
* @name binance#watchLiquidations
* @description watch the public liquidations of a trading pair
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Liquidation-Order-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Liquidation-Order-Streams
* @param {string} symbol unified CCXT market symbol
* @param {int} [since] the earliest time in ms to fetch liquidations for
* @param {int} [limit] the maximum number of liquidation structures to retrieve
* @param {object} [params] exchange specific parameters for the bitmex api endpoint
* @returns {object} an array of [liquidation structures]{@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure}
*/
async watchLiquidations(symbol, since = undefined, limit = undefined, params = {}) {
return await this.watchLiquidationsForSymbols([symbol], since, limit, params);
}
/**
* @method
* @name binance#watchLiquidationsForSymbols
* @description watch the public liquidations of a trading pair
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Liquidation-Order-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Liquidation-Order-Streams
* @param {string[]} symbols list of unified market symbols
* @param {int} [since] the earliest time in ms to fetch liquidations for
* @param {int} [limit] the maximum number of liquidation structures to retrieve
* @param {object} [params] exchange specific parameters for the bitmex api endpoint
* @returns {object} an array of [liquidation structures]{@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure}
*/
async watchLiquidationsForSymbols(symbols = undefined, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets();
const subscriptionHashes = [];
const messageHashes = [];
let streamHash = 'liquidations';
symbols = this.marketSymbols(symbols, undefined, true, true);
if (this.isEmpty(symbols)) {
subscriptionHashes.push('!' + 'forceOrder@arr');
messageHashes.push('liquidations');
}
else {
for (let i = 0; i < symbols.length; i++) {
const market = this.market(symbols[i]);
subscriptionHashes.push(market['lowercaseId'] + '@forceOrder');
messageHashes.push('liquidations::' + symbols[i]);
}
streamHash += '::' + symbols.join(',');
}
const firstMarket = this.getMarketFromSymbols(symbols);
let type = undefined;
[type, params] = this.handleMarketTypeAndParams('watchLiquidationsForSymbols', firstMarket, params);
if (type === 'spot') {
throw new BadRequest(this.id + ' watchLiquidationsForSymbols is not supported for spot symbols');
}
let subType = undefined;
[subType, params] = this.handleSubTypeAndParams('watchLiquidationsForSymbols', firstMarket, params);
if (this.isLinear(type, subType)) {
type = 'future';
}
else if (this.isInverse(type, subType)) {
type = 'delivery';
}
const numSubscriptions = subscriptionHashes.length;
const url = this.urls['api']['ws'][type] + '/' + this.stream(type, streamHash, numSubscriptions);
const requestId = this.requestId(url);
const request = {
'method': 'SUBSCRIBE',
'params': subscriptionHashes,
'id': requestId,
};
const subscribe = {
'id': requestId,
};
const newLiquidations = await this.watchMultiple(url, messageHashes, this.extend(request, params), subscriptionHashes, subscribe);
if (this.newUpdates) {
return newLiquidations;
}
return this.filterBySymbolsSinceLimit(this.liquidations, symbols, since, limit, true);
}
handleLiquidation(client, message) {
//
// future
// {
// "e":"forceOrder",
// "E":1698871323061,
// "o":{
// "s":"BTCUSDT",
// "S":"BUY",
// "o":"LIMIT",
// "f":"IOC",
// "q":"1.437",
// "p":"35100.81",
// "ap":"34959.70",
// "X":"FILLED",
// "l":"1.437",
// "z":"1.437",
// "T":1698871323059
// }
// }
// delivery
// {
// "e":"forceOrder", // Event Type
// "E": 1591154240950, // Event Time
// "o":{
// "s":"BTCUSD_200925", // Symbol
// "ps": "BTCUSD", // Pair
// "S":"SELL", // Side
// "o":"LIMIT", // Order Type
// "f":"IOC", // Time in Force
// "q":"1", // Original Quantity
// "p":"9425.5", // Price
// "ap":"9496.5", // Average Price
// "X":"FILLED", // Order Status
// "l":"1", // Order Last Filled Quantity
// "z":"1", // Order Filled Accumulated Quantity
// "T": 1591154240949, // Order Trade Time
// }
// }
//
const rawLiquidation = this.safeValue(message, 'o', {});
const marketId = this.safeString(rawLiquidation, 's');
const market = this.safeMarket(marketId, undefined, '', 'contract');
const symbol = market['symbol'];
const liquidation = this.parseWsLiquidation(rawLiquidation, market);
let liquidations = this.safeValue(this.liquidations, symbol);
if (liquidations === undefined) {
const limit = this.safeInteger(this.options, 'liquidationsLimit', 1000);
liquidations = new ArrayCache(limit);
}
liquidations.append(liquidation);
this.liquidations[symbol] = liquidations;
client.resolve([liquidation], 'liquidations');
client.resolve([liquidation], 'liquidations::' + symbol);
}
parseWsLiquidation(liquidation, market = undefined) {
//
// future
// {
// "s":"BTCUSDT",
// "S":"BUY",
// "o":"LIMIT",
// "f":"IOC",
// "q":"1.437",
// "p":"35100.81",
// "ap":"34959.70",
// "X":"FILLED",
// "l":"1.437",
// "z":"1.437",
// "T":1698871323059
// }
// delivery
// {
// "s":"BTCUSD_200925", // Symbol
// "ps": "BTCUSD", // Pair
// "S":"SELL", // Side
// "o":"LIMIT", // Order Type
// "f":"IOC", // Time in Force
// "q":"1", // Original Quantity
// "p":"9425.5", // Price
// "ap":"9496.5", // Average Price
// "X":"FILLED", // Order Status
// "l":"1", // Order Last Filled Quantity
// "z":"1", // Order Filled Accumulated Quantity
// "T": 1591154240949, // Order Trade Time
// }
// myLiquidation
// {
// "s":"BTCUSDT", // Symbol
// "c":"TEST", // Client Order Id
// // special client order id:
// // starts with "autoclose-": liquidation order
// // "adl_autoclose": ADL auto close order
// // "settlement_autoclose-": settlement order for delisting or delivery
// "S":"SELL", // Side
// "o":"TRAILING_STOP_MARKET", // Order Type
// "f":"GTC", // Time in Force
// "q":"0.001", // Original Quantity
// "p":"0", // Original Price
// "ap":"0", // Average Price
// "sp":"7103.04", // Stop Price. Please ignore with TRAILING_STOP_MARKET order
// "x":"NEW", // Execution Type
// "X":"NEW", // Order Status
// "i":8886774, // Order Id
// "l":"0", // Order Last Filled Quantity
// "z":"0", // Order Filled Accumulated Quantity
// "L":"0", // Last Filled Price
// "N":"USDT", // Commission Asset, will not push if no commission
// "n":"0", // Commission, will not push if no commission
// "T":1568879465650, // Order Trade Time
// "t":0, // Trade Id
// "b":"0", // Bids Notional
// "a":"9.91", // Ask Notional
// "m":false, // Is this trade the maker side?
// "R":false, // Is this reduce only
// "wt":"CONTRACT_PRICE", // Stop Price Working Type
// "ot":"TRAILING_STOP_MARKET",// Original Order Type
// "ps":"LONG", // Position Side
// "cp":false, // If Close-All, pushed with conditional order
// "AP":"7476.89", // Activation Price, only puhed with TRAILING_STOP_MARKET order
// "cr":"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order
// "pP": false, // If price protection is turned on
// "si": 0, // ignore
// "ss": 0, // ignore
// "rp":"0", // Realized Profit of the trade
// "V":"EXPIRE_TAKER", // STP mode
// "pm":"OPPONENT", // Price match mode
// "gtd":0 // TIF GTD order auto cancel time
// }
//
const marketId = this.safeString(liquidation, 's');
market = this.safeMarket(marketId, market);
const timestamp = this.safeInteger(liquidation, 'T');
return this.safeLiquidation({
'info': liquidation,
'symbol': this.safeSymbol(marketId, market),
'contracts': this.safeNumber(liquidation, 'l'),
'contractSize': this.safeNumber(market, 'contractSize'),
'price': this.safeNumber(liquidation, 'ap'),
'baseValue': undefined,
'quoteValue': undefined,
'timestamp': timestamp,
'datetime': this.iso8601(timestamp),
});
}
/**
* @method
* @name binance#watchMyLiquidations
* @description watch the private liquidations of a trading pair
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update
* @param {string} symbol unified CCXT market symbol
* @param {int} [since] the earliest time in ms to fetch liquidations for
* @param {int} [limit] the maximum number of liquidation structures to retrieve
* @param {object} [params] exchange specific parameters for the bitmex api endpoint
* @returns {object} an array of [liquidation structures]{@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure}
*/
async watchMyLiquidations(symbol, since = undefined, limit = undefined, params = {}) {
return this.watchMyLiquidationsForSymbols([symbol], since, limit, params);
}
/**
* @method
* @name binance#watchMyLiquidationsForSymbols
* @description watch the private liquidations of a trading pair
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update
* @param {string[]} symbols list of unified market symbols
* @param {int} [since] the earliest time in ms to fetch liquidations for
* @param {int} [limit] the maximum number of liquidation structures to retrieve
* @param {object} [params] exchange specific parameters for the bitmex api endpoint
* @returns {object} an array of [liquidation structures]{@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure}
*/
async watchMyLiquidationsForSymbols(symbols, since = undefined, limit = undefined, params = {}) {
await this.loadMarkets();
symbols = this.marketSymbols(symbols, undefined, true, true, true);
const market = this.getMarketFromSymbols(symbols);
const messageHashes = ['myLiquidations'];
if (!this.isEmpty(symbols)) {
for (let i = 0; i < symbols.length; i++) {
const symbol = symbols[i];
messageHashes.push('myLiquidations::' + symbol);
}
}
let type = undefined;
[type, params] = this.handleMarketTypeAndParams('watchMyLiquidationsForSymbols', market, params);
let subType = undefined;
[subType, params] = this.handleSubTypeAndParams('watchMyLiquidationsForSymbols', market, params);
if (this.isLinear(type, subType)) {
type = 'future';
}
else if (this.isInverse(type, subType)) {
type = 'delivery';
}
await this.authenticate(params);
const url = this.urls['api']['ws'][type] + '/' + this.options[type]['listenKey'];
const message = undefined;
const newLiquidations = await this.watchMultiple(url, messageHashes, message, [type]);
if (this.newUpdates) {
return newLiquidations;
}
return this.filterBySymbolsSinceLimit(this.liquidations, symbols, since, limit);
}
handleMyLiquidation(client, message) {
//
// {
// "s":"BTCUSDT", // Symbol
// "c":"TEST", // Client Order Id
// // special client order id:
// // starts with "autoclose-": liquidation order
// // "adl_autoclose": ADL auto close order
// // "settlement_autoclose-": settlement order for delisting or delivery
// "S":"SELL", // Side
// "o":"TRAILING_STOP_MARKET", // Order Type
// "f":"GTC", // Time in Force
// "q":"0.001", // Original Quantity
// "p":"0", // Original Price
// "ap":"0", // Average Price
// "sp":"7103.04", // Stop Price. Please ignore with TRAILING_STOP_MARKET order
// "x":"NEW", // Execution Type
// "X":"NEW", // Order Status
// "i":8886774, // Order Id
// "l":"0", // Order Last Filled Quantity
// "z":"0", // Order Filled Accumulated Quantity
// "L":"0", // Last Filled Price
// "N":"USDT", // Commission Asset, will not push if no commission
// "n":"0", // Commission, will not push if no commission
// "T":1568879465650, // Order Trade Time
// "t":0, // Trade Id
// "b":"0", // Bids Notional
// "a":"9.91", // Ask Notional
// "m":false, // Is this trade the maker side?
// "R":false, // Is this reduce only
// "wt":"CONTRACT_PRICE", // Stop Price Working Type
// "ot":"TRAILING_STOP_MARKET",// Original Order Type
// "ps":"LONG", // Position Side
// "cp":false, // If Close-All, pushed with conditional order
// "AP":"7476.89", // Activation Price, only puhed with TRAILING_STOP_MARKET order
// "cr":"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order
// "pP": false, // If price protection is turned on
// "si": 0, // ignore
// "ss": 0, // ignore
// "rp":"0", // Realized Profit of the trade
// "V":"EXPIRE_TAKER", // STP mode
// "pm":"OPPONENT", // Price match mode
// "gtd":0 // TIF GTD order auto cancel time
// }
//
const orderType = this.safeString(message, 'o');
if (orderType !== 'LIQUIDATION') {
return;
}
const marketId = this.safeString(message, 's');
const market = this.safeMarket(marketId);
const symbol = this.safeSymbol(marketId);
const liquidation = this.parseWsLiquidation(message, market);
let myLiquidations = this.safeValue(this.myLiquidations, symbol);
if (myLiquidations === undefined) {
const limit = this.safeInteger(this.options, 'myLiquidationsLimit', 1000);
myLiquidations = new ArrayCache(limit);
}
myLiquidations.append(liquidation);
this.myLiquidations[symbol] = myLiquidations;
client.resolve([liquidation], 'myLiquidations');
client.resolve([liquidation], 'myLiquidations::' + symbol);
}
/**
* @method
* @name binance#watchOrderBook
* @description watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @param {string} symbol unified symbol of the market to fetch the order book for
* @param {int} [limit] the maximum amount of order book entries to return
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/#/?id=order-book-structure} indexed by market symbols
*/
async watchOrderBook(symbol, limit = undefined, params = {}) {
//
// todo add support for <levels>-snapshots (depth)
// https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams // <symbol>@depth<levels>@100ms or <symbol>@depth<levels> (1000ms)
// valid <levels> are 5, 10, or 20
//
// default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000
//
// notice the differences between trading futures and spot trading
// the algorithms use different urls in step 1
// delta caching and merging also differs in steps 4, 5, 6
//
// spot/margin
// https://binance-docs.github.io/apidocs/spot/en/#how-to-manage-a-local-order-book-correctly
//
// 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth.
// 2. Buffer the events you receive from the stream.
// 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 .
// 4. Drop any event where u is <= lastUpdateId in the snapshot.
// 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1.
// 6. While listening to the stream, each new event's U should be equal to the previous event's u+1.
// 7. The data in each event is the absolute quantity for a price level.
// 8. If the quantity is 0, remove the price level.
// 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal.
//
// futures
// https://binance-docs.github.io/apidocs/futures/en/#how-to-manage-a-local-order-book-correctly
//
// 1. Open a stream to wss://fstream.binance.com/stream?streams=btcusdt@depth.
// 2. Buffer the events you receive from the stream. For same price, latest received update covers the previous one.
// 3. Get a depth snapshot from https://fapi.binance.com/fapi/v1/depth?symbol=BTCUSDT&limit=1000 .
// 4. Drop any event where u is < lastUpdateId in the snapshot.
// 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId
// 6. While listening to the stream, each new event's pu should be equal to the previous event's u, otherwise initialize the process from step 3.
// 7. The data in each event is the absolute quantity for a price level.
// 8. If the quantity is 0, remove the price level.
// 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal.
//
return await this.watchOrderBookForSymbols([symbol], limit, params);
}
/**
* @method
* @name binance#watchOrderBookForSymbols
* @description watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @param {string[]} symbols unified array of symbols
* @param {int} [limit] the maximum amount of order book entries to return
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/#/?id=order-book-structure} indexed by market symbols
*/
async watchOrderBookForSymbols(symbols, limit = undefined, params = {}) {
await this.loadMarkets();
symbols = this.marketSymbols(symbols, undefined, false, true, true);
const firstMarket = this.market(symbols[0]);
let type = firstMarket['type'];
if (firstMarket['contract']) {
type = firstMarket['linear'] ? 'future' : 'delivery';
}
const name = 'depth';
let streamHash = 'multipleOrderbook';
if (symbols !== undefined) {
const symbolsLength = symbols.length;
if (symbolsLength > 200) {
throw new BadRequest(this.id + ' watchOrderBookForSymbols() accepts 200 symbols at most. To watch more symbols call watchOrderBookForSymbols() multiple times');
}
streamHash += '::' + symbols.join(',');
}
const watchOrderBookRate = this.safeString(this.options, 'watchOrderBookRate', '100');
const subParams = [];
const messageHashes = [];
for (let i = 0; i < symbols.length; i++) {
const symbol = symbols[i];
const market = this.market(symbol);
messageHashes.push('orderbook::' + symbol);
const subscriptionHash = market['lowercaseId'] + '@' + name;
const symbolHash = subscriptionHash + '@' + watchOrderBookRate + 'ms';
subParams.push(symbolHash);
}
const messageHashesLength = messageHashes.length;
const url = this.urls['api']['ws'][type] + '/' + this.stream(type, streamHash, messageHashesLength);
const requestId = this.requestId(url);
const request = {
'method': 'SUBSCRIBE',
'params': subParams,
'id': requestId,
};
const subscription = {
'id': requestId.toString(),
'name': name,
'symbols': symbols,
'method': this.handleOrderBookSubscription,
'limit': limit,
'type': type,
'params': params,
};
const orderbook = await this.watchMultiple(url, messageHashes, this.extend(request, params), messageHashes, subscription);
return orderbook.limit();
}
/**
* @method
* @name binance#unWatchOrderBookForSymbols
* @description unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @param {string[]} symbols unified array of symbols
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/#/?id=order-book-structure} indexed by market symbols
*/
async unWatchOrderBookForSymbols(symbols, params = {}) {
await this.loadMarkets();
symbols = this.marketSymbols(symbols, undefined, false, true, true);
const firstMarket = this.market(symbols[0]);
let type = firstMarket['type'];
if (firstMarket['contract']) {
type = firstMarket['linear'] ? 'future' : 'delivery';
}
const name = 'depth';
let streamHash = 'multipleOrderbook';
if (symbols !== undefined) {
streamHash += '::' + symbols.join(',');
}
const watchOrderBookRate = this.safeString(this.options, 'watchOrderBookRate', '100');
const subParams = [];
const subMessageHashes = [];
const messageHashes = [];
for (let i = 0; i < symbols.length; i++) {
const symbol = symbols[i];
const market = this.market(symbol);
subMessageHashes.push('orderbook::' + symbol);
messageHashes.push('unsubscribe:orderbook:' + symbol);
const subscriptionHash = market['lowercaseId'] + '@' + name;
const symbolHash = subscriptionHash + '@' + watchOrderBookRate + 'ms';
subParams.push(symbolHash);
}
const messageHashesLength = subMessageHashes.length;
const url = this.urls['api']['ws'][type] + '/' + this.stream(type, streamHash, messageHashesLength);
const requestId = this.requestId(url);
const request = {
'method': 'UNSUBSCRIBE',
'params': subParams,
'id': requestId,
};
const subscription = {
'unsubscribe': true,
'id': requestId.toString(),
'symbols': symbols,
'subMessageHashes': subMessageHashes,
'messageHashes': messageHashes,
'topic': 'orderbook',
};
return await this.watchMultiple(url, messageHashes, this.extend(request, params), messageHashes, subscription);
}
/**
* @method
* @name binance#unWatchOrderBook
* @description unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
* @see https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
* @see https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
* @param {string} symbol unified array of symbols
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/#/?id=order-book-structure} indexed by market symbols
*/
async unWatchOrderBook(symbol, params = {}) {
return await this.unWatchOrderBookForSymbols([symbol], params);
}
/**
* @method
* @name binance#fetchOrderBookWs
* @description fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @see https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#order-book
* @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/websocket-api/Order-Book
* @param {string} symbol unified symbol of the market to fetch the order book for
* @param {int} [limit] the maximum amount of order book entries to return
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/#/?id=order-book-structure} indexed by market symbols
*/
async fetchOrderBookWs(symbol, limit = undefined, params = {}) {
await this.loadMarkets();
const market = this.market(symbol);
const payload = {
'symbol': market['id'],
};
if (limit !== undefined) {
payload['limit'] = limit;
}
const marketType = this.getMarketType('fetchOrderBookWs', market, params);
if (marketType !== 'future') {
throw new BadRequest(this.id + ' fetchOrderBookWs only supports swap markets');
}
const url = this.urls['api']['ws']['ws-api'][marketType];
const requestId = this.requestId(url);
const messageHash = requestId.toString();
let returnRateLimits = false;
[returnRateLimits, params] = this.handleOptionAndParams(params, 'createOrderWs', 'returnRateLimits', false);
payload['returnRateLimits'] = returnRateLimits;
params = this.omit(params, 'test');
const message = {
'id': messageHash,
'method': 'depth',
'params': this.signParams(this.extend(payload, params)),
};
const subscription = {
'method': this.handleFetchOrderBook,
};
const orderbook = await this.watch(url, messageHash, message, messageHash, subscription);
orderbook['symbol'] = market['symbol'];
return orderbook;
}
handleFetchOrderBook(client, message) {
//
// {
// "id":"51e2affb-0aba-4821-ba75-f2625006eb43",
// "status":200,
// "result":{
// "lastUpdateId":1027024,
// "E":1589436922972,
// "T":1589436922959,
// "bids":[
// [
// "4.00000000",
// "431.00000000"
// ]
// ],
// "asks":[
// [
// "4.00000200",
// "12.00000000"
// ]
// ]
// }
// }
//
const messageHash = this.safeString(message, 'id');
const result = this.safeDict(message, 'result');
const timestamp = this.safeInteger(result, 'T');
const orderbook = this.parseOrderBook(result, undefined, timestamp);
orderbook['nonce'] = this.safeInteger2(result, 'lastUpdateId', 'u');
client.resolve(orderbook, messageHash);
}
async fetchOrderBookSnapshot(client, message, subscription) {
const symbol = this.safeString(subscription, 'symbol');
const messageHash = 'orderbook::' + symbol;
try {
const defaultLimit = this.safeInteger(this.options, 'watchOrderBookLimit', 1000);
const type = this.safeValue(subscription, 'type');
const limit = this.safeInteger(subscription, 'limit', defaultLimit);
const params = this.safeValue(subscription, 'params');
// 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 .
// todo: this is a synch blocking call - make it async
// default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000
const snapshot = await this.fetchRestOrderBookSafe(symbol, limit, params);
if (this.safeValue(this.orderbooks, symbol) === undefined) {
// if the orderbook is dropped before the snapshot is received
return;
}
const orderbook = this.orderbooks[symbol];
orderbook.reset(snapshot);
// unroll the accumulated deltas
const messages = orderbook.cache;
orderbook.cache = [];
for (let i = 0; i < messages.length; i++) {
const messageItem = messages[i];
const U = this.safeInteger(messageItem, 'U');
const u = this.safeInteger(messageItem, 'u');
const pu = this.safeInteger(messageItem, 'pu');
if (type === 'future') {
// 4. Drop any event where u is < lastUpdateId in the snapshot
if (u < orderbook['nonce']) {
continue;
}
// 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId
if ((U <= orderbook['nonce']) && (u >= orderbook['nonce']) || (pu === orderbook['nonce'])) {
this.handleOrderBookMessage(client, messageItem, orderbook);
}
}
else {
// 4. Drop any event where u is <= lastUpdateId in the snapshot
if (u <= orderbook['nonce']) {
continue;
}
// 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1
if (((U - 1) <= orderbook['nonce']) && ((u - 1) >= orderbook['nonce'])) {
this.handleOrderBookMessage(client, messageItem, orderbook);
}
}
}
this.orderbooks[symbol] = orderbook;
client.resolve(orderbook, messageHash);
}
catch (e) {
delete client.subscriptions[messageHash];
client.reject(e, messageHash);
}
}
handleDelta(bookside, delta) {
const price = this.safeFloat(delta, 0);
const amount = this.safeFloat(delta, 1);
bookside.store(price, amount);
}
handleDeltas(bookside, deltas) {
for (let i = 0; i < deltas.length; i++) {
this.handleDelta(bookside, deltas[i]);
}
}
handleOrderBookMessage(client, message, orderbook) {
const u = this.safeInteger(message, 'u');
this.handleDeltas(orderbook['asks'], this.safeValue(message, 'a', []));
this.handleDeltas(orderbook['bids'], this.safeValue(message, 'b', []));
orderbook['nonce'] = u;
const timestamp = this.safeInteger(message, 'E');
orderbook['timestamp'] = timestamp;
orderbook['datetime'] = this.iso8601(timestamp);
return orderbook;
}
handleOrderBook(client, message) {
//
// initial snapshot is fetched with ccxt's fetchOrderBook
// the feed does not include a snapshot, just the deltas
//
// {
// "e": "depthUpdate", // Event type
// "E": 1577554482280, // Event time
// "s": "BNBBTC", // Symbol
// "U": 157, // First update ID in event
// "u": 160, // Final update ID in event
// "b": [ // bids
// [ "0.0024", "10" ], // price, size
// ],
// "a": [ // asks
// [ "0.0026", "100" ], // price, size
// ]
// }
//
const isSpot = (client.url.indexOf('/stream') > -1);
const marketType = (isSpot) ? 'spot' : 'contract';
const marketId = this.safeString(message, 's');
const market = this.safeMarket(marketId, undefined, undefined, marketType);
const symbol = market['symbol'];
const messageHash = 'orderbook::' + symbol;
if (!(symbol in this.orderbooks)) {
//
// https://github.com/ccxt/ccxt/issues/6672
//
// Sometimes Binance sends the first delta before the subscription
// confirmation arrives. At that point the orderbook is not
// initialized yet and the snapshot has not been requested yet
// therefore it is safe to drop these premature messages.
//
return;
}
const orderbook = this.orderbooks[symbol];
const nonce = this.safeInteger(orderbook, 'nonce');
if (nonce === undefined) {
// 2. Buffer the events you receive from the stream.
orderbook.cache.push(message);
}
else {
try {
const U = this.safeInteger(message, 'U');
const u = this.safeInteger(message, 'u');
const pu = this.safeInteger(message, 'pu');
if (pu === undefined) {
// spot
// 4. Drop any event where u is <= lastUpdateId in the snapshot
if (u > orderbook['nonce']) {
const timestamp = this.safeInteger(orderbook, 'timestamp');
let conditional = undefined;
if (timestamp === undefined) {
// 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1