'use strict'; // --------------------------------------------------------------------------- const Exchange = require ('./base/Exchange'); const { ExchangeError, ArgumentsRequired, ExchangeNotAvailable, InsufficientFunds, OrderNotFound, InvalidOrder, DDoSProtection, InvalidNonce, AuthenticationError, InvalidAddress, RateLimitExceeded, PermissionDenied } = require ('./base/errors'); const { ROUND, TRUNCATE } = require ('./base/functions/number'); // --------------------------------------------------------------------------- module.exports = class binance extends Exchange { describe () { return this.deepExtend (super.describe (), { 'id': 'binance', 'name': 'Binance', 'countries': [ 'JP', 'MT' ], // Japan, Malta 'rateLimit': 500, 'certified': true, 'pro': true, // new metainfo interface 'has': { 'fetchDepositAddress': true, 'CORS': false, 'fetchBidsAsks': true, 'fetchTickers': true, 'fetchTime': true, 'fetchOHLCV': true, 'fetchMyTrades': true, 'fetchOrder': true, 'fetchOrders': true, 'fetchOpenOrders': true, 'fetchClosedOrders': 'emulated', 'withdraw': true, 'fetchFundingFees': true, 'fetchDeposits': true, 'fetchWithdrawals': true, 'fetchTransactions': false, 'fetchTradingFee': true, 'fetchTradingFees': true, }, 'timeframes': { '1m': '1m', '3m': '3m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1h', '2h': '2h', '4h': '4h', '6h': '6h', '8h': '8h', '12h': '12h', '1d': '1d', '3d': '3d', '1w': '1w', '1M': '1M', }, 'urls': { 'logo': 'https://user-images.githubusercontent.com/1294454/29604020-d5483cdc-87ee-11e7-94c7-d1a8d9169293.jpg', 'api': { 'web': 'https://www.binance.com', 'wapi': 'https://api.binance.com/wapi/v3', 'sapi': 'https://api.binance.com/sapi/v1', 'fapiPublic': 'https://fapi.binance.com/fapi/v1', 'fapiPrivate': 'https://fapi.binance.com/fapi/v1', 'public': 'https://api.binance.com/api/v3', 'private': 'https://api.binance.com/api/v3', 'v3': 'https://api.binance.com/api/v3', 'v1': 'https://api.binance.com/api/v1', }, 'www': 'https://www.binance.com', 'referral': 'https://www.binance.com/?ref=10205187', 'doc': [ 'https://binance-docs.github.io/apidocs/spot/en', ], 'api_management': 'https://www.binance.com/en/usercenter/settings/api-management', 'fees': 'https://www.binance.com/en/fee/schedule', }, 'api': { 'web': { 'get': [ 'exchange/public/product', 'assetWithdraw/getAllAsset.html', ], }, // the API structure below will need 3-layer apidefs 'sapi': { 'get': [ 'accountSnapshot', // these endpoints require this.apiKey 'margin/asset', 'margin/pair', 'margin/allAssets', 'margin/allPairs', 'margin/priceIndex', // these endpoints require this.apiKey + this.secret 'asset/assetDividend', 'margin/loan', 'margin/repay', 'margin/account', 'margin/transfer', 'margin/interestHistory', 'margin/forceLiquidationRec', 'margin/order', 'margin/openOrders', 'margin/allOrders', 'margin/myTrades', 'margin/maxBorrowable', 'margin/maxTransferable', 'futures/transfer', // https://binance-docs.github.io/apidocs/spot/en/#withdraw-sapi 'capital/config/getall', // get networks for withdrawing USDT ERC20 vs USDT Omni 'capital/deposit/address', 'capital/deposit/hisrec', 'capital/deposit/subAddress', 'capital/deposit/subHisrec', 'capital/withdraw/history', 'sub-account/futures/account', 'sub-account/futures/accountSummary', 'sub-account/futures/positionRisk', 'sub-account/margin/account', 'sub-account/margin/accountSummary', 'sub-account/status', // lending endpoints 'lending/daily/product/list', 'lending/daily/userLeftQuota', 'lending/daily/userRedemptionQuota', 'lending/daily/token/position', 'lending/union/account', 'lending/union/purchaseRecord', 'lending/union/redemptionRecord', 'lending/union/interestHistory', ], 'post': [ 'asset/dust', 'account/disableFastWithdrawSwitch', 'account/enableFastWithdrawSwitch', 'capital/withdraw/apply', 'margin/transfer', 'margin/loan', 'margin/repay', 'margin/order', 'sub-account/margin/enable', 'sub-account/margin/enable', 'sub-account/futures/enable', 'userDataStream', 'futures/transfer', // lending 'lending/daily/purchase', 'lending/daily/redeem', ], 'put': [ 'userDataStream', ], 'delete': [ 'margin/order', 'userDataStream', ], }, 'wapi': { 'post': [ 'withdraw', 'sub-account/transfer', ], 'get': [ 'depositHistory', 'withdrawHistory', 'depositAddress', 'accountStatus', 'systemStatus', 'apiTradingStatus', 'userAssetDribbletLog', 'tradeFee', 'assetDetail', 'sub-account/list', 'sub-account/transfer/history', 'sub-account/assets', ], }, 'fapiPublic': { 'get': [ 'ping', 'time', 'exchangeInfo', 'depth', 'trades', 'historicalTrades', 'aggTrades', 'klines', 'fundingRate', 'premiumIndex', 'ticker/24hr', 'ticker/price', 'ticker/bookTicker', 'allForceOrders', 'openInterest', 'leverageBracket', ], }, 'fapiPrivate': { 'get': [ 'allOrders', 'openOrder', 'openOrders', 'order', 'account', 'balance', 'positionMargin/history', 'positionRisk', 'userTrades', 'income', ], 'post': [ 'positionMargin', 'marginType', 'order', 'leverage', 'listenKey', ], 'put': [ 'listenKey', ], 'delete': [ 'order', 'allOpenOrders', 'listenKey', ], }, 'v3': { 'get': [ 'ticker/price', 'ticker/bookTicker', ], }, 'public': { 'get': [ 'ping', 'time', 'depth', 'trades', 'aggTrades', 'historicalTrades', 'klines', 'ticker/24hr', 'ticker/price', 'ticker/bookTicker', 'exchangeInfo', ], 'put': [ 'userDataStream' ], 'post': [ 'userDataStream' ], 'delete': [ 'userDataStream' ], }, 'private': { 'get': [ 'allOrderList', // oco 'openOrderList', // oco 'orderList', // oco 'order', 'openOrders', 'allOrders', 'account', 'myTrades', ], 'post': [ 'order/oco', 'order', 'order/test', ], 'delete': [ 'orderList', // oco 'order', ], }, }, 'fees': { 'trading': { 'tierBased': false, 'percentage': true, 'taker': 0.001, 'maker': 0.001, }, }, 'commonCurrencies': { 'BCC': 'BCC', // kept for backward-compatibility https://github.com/ccxt/ccxt/issues/4848 'YOYO': 'YOYOW', }, // exchange-specific options 'options': { 'fetchTradesMethod': 'publicGetAggTrades', // publicGetTrades, publicGetHistoricalTrades 'fetchTickersMethod': 'publicGetTicker24hr', 'defaultTimeInForce': 'GTC', // 'GTC' = Good To Cancel (default), 'IOC' = Immediate Or Cancel 'defaultLimitOrderType': 'limit', // or 'limit_maker' 'defaultType': 'spot', // 'spot', 'future' 'hasAlreadyAuthenticatedSuccessfully': false, 'warnOnFetchOpenOrdersWithoutSymbol': true, 'recvWindow': 5 * 1000, // 5 sec, binance default 'timeDifference': 0, // the difference between system clock and Binance clock 'adjustForTimeDifference': false, // controls the adjustment logic upon instantiation 'parseOrderToPrecision': false, // force amounts and costs in parseOrder to precision 'newOrderRespType': { 'market': 'FULL', // 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills 'limit': 'RESULT', // we change it from 'ACK' by default to 'RESULT' }, }, 'exceptions': { 'API key does not exist': AuthenticationError, 'Order would trigger immediately.': InvalidOrder, 'Account has insufficient balance for requested action.': InsufficientFunds, 'Rest API trading is not enabled.': ExchangeNotAvailable, "You don't have permission.": PermissionDenied, // {"msg":"You don't have permission.","success":false} 'Market is closed.': ExchangeNotAvailable, // {"code":-1013,"msg":"Market is closed."} '-1000': ExchangeNotAvailable, // {"code":-1000,"msg":"An unknown error occured while processing the request."} '-1003': RateLimitExceeded, // {"code":-1003,"msg":"Too much request weight used, current limit is 1200 request weight per 1 MINUTE. Please use the websocket for live updates to avoid polling the API."} '-1013': InvalidOrder, // createOrder -> 'invalid quantity'/'invalid price'/MIN_NOTIONAL '-1021': InvalidNonce, // 'your time is ahead of server' '-1022': AuthenticationError, // {"code":-1022,"msg":"Signature for this request is not valid."} '-1100': InvalidOrder, // createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price' '-1104': ExchangeError, // Not all sent parameters were read, read 8 parameters but was sent 9 '-1128': ExchangeError, // {"code":-1128,"msg":"Combination of optional parameters invalid."} '-2010': ExchangeError, // generic error code for createOrder -> 'Account has insufficient balance for requested action.', {"code":-2010,"msg":"Rest API trading is not enabled."}, etc... '-2011': OrderNotFound, // cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER' '-2013': OrderNotFound, // fetchOrder (1, 'BTC/USDT') -> 'Order does not exist' '-2014': AuthenticationError, // { "code":-2014, "msg": "API-key format invalid." } '-2015': AuthenticationError, // "Invalid API-key, IP, or permissions for action." }, }); } nonce () { return this.milliseconds () - this.options['timeDifference']; } async fetchTime (params = {}) { const type = this.safeString2 (this.options, 'fetchTime', 'defaultType', 'spot'); const method = (type === 'spot') ? 'publicGetTime' : 'fapiPublicGetTime'; const response = await this[method] (params); return this.safeInteger (response, 'serverTime'); } async loadTimeDifference () { const serverTime = await this.fetchTime (); const after = this.milliseconds (); this.options['timeDifference'] = after - serverTime; return this.options['timeDifference']; } async fetchMarkets (params = {}) { const defaultType = this.safeString2 (this.options, 'fetchMarkets', 'defaultType', 'spot'); const type = this.safeString (params, 'type', defaultType); const query = this.omit (params, 'type'); const method = (type === 'spot') ? 'publicGetExchangeInfo' : 'fapiPublicGetExchangeInfo'; const response = await this[method] (query); // // spot // // { // "timezone":"UTC", // "serverTime":1575416692969, // "rateLimits":[ // {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200}, // {"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":100}, // {"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":200000} // ], // "exchangeFilters":[], // "symbols":[ // { // "symbol":"ETHBTC", // "status":"TRADING", // "baseAsset":"ETH", // "baseAssetPrecision":8, // "quoteAsset":"BTC", // "quotePrecision":8, // "baseCommissionPrecision":8, // "quoteCommissionPrecision":8, // "orderTypes":["LIMIT","LIMIT_MAKER","MARKET","STOP_LOSS_LIMIT","TAKE_PROFIT_LIMIT"], // "icebergAllowed":true, // "ocoAllowed":true, // "quoteOrderQtyMarketAllowed":true, // "isSpotTradingAllowed":true, // "isMarginTradingAllowed":true, // "filters":[ // {"filterType":"PRICE_FILTER","minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}, // {"filterType":"PERCENT_PRICE","multiplierUp":"5","multiplierDown":"0.2","avgPriceMins":5}, // {"filterType":"LOT_SIZE","minQty":"0.00100000","maxQty":"100000.00000000","stepSize":"0.00100000"}, // {"filterType":"MIN_NOTIONAL","minNotional":"0.00010000","applyToMarket":true,"avgPriceMins":5}, // {"filterType":"ICEBERG_PARTS","limit":10}, // {"filterType":"MARKET_LOT_SIZE","minQty":"0.00000000","maxQty":"63100.00000000","stepSize":"0.00000000"}, // {"filterType":"MAX_NUM_ALGO_ORDERS","maxNumAlgoOrders":5} // ] // }, // ], // } // // futures (fapi) // // { // "timezone":"UTC", // "serverTime":1575417244353, // "rateLimits":[ // {"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200}, // {"rateLimitType":"ORDERS","interval":"MINUTE","intervalNum":1,"limit":1200} // ], // "exchangeFilters":[], // "symbols":[ // { // "symbol":"BTCUSDT", // "status":"TRADING", // "maintMarginPercent":"2.5000", // "requiredMarginPercent":"5.0000", // "baseAsset":"BTC", // "quoteAsset":"USDT", // "pricePrecision":2, // "quantityPrecision":3, // "baseAssetPrecision":8, // "quotePrecision":8, // "filters":[ // {"minPrice":"0.01","maxPrice":"100000","filterType":"PRICE_FILTER","tickSize":"0.01"}, // {"stepSize":"0.001","filterType":"LOT_SIZE","maxQty":"1000","minQty":"0.001"}, // {"stepSize":"0.001","filterType":"MARKET_LOT_SIZE","maxQty":"1000","minQty":"0.001"}, // {"limit":200,"filterType":"MAX_NUM_ORDERS"}, // {"multiplierDown":"0.8500","multiplierUp":"1.1500","multiplierDecimal":"4","filterType":"PERCENT_PRICE"} // ], // "orderTypes":["LIMIT","MARKET","STOP"], // "timeInForce":["GTC","IOC","FOK","GTX"] // } // ] // } // if (this.options['adjustForTimeDifference']) { await this.loadTimeDifference (); } const markets = this.safeValue (response, 'symbols'); const result = []; for (let i = 0; i < markets.length; i++) { const market = markets[i]; const future = ('maintMarginPercent' in market); const spot = !future; const marketType = spot ? 'spot' : 'future'; const id = this.safeString (market, 'symbol'); const lowercaseId = this.safeStringLower (market, 'symbol'); const baseId = market['baseAsset']; const quoteId = market['quoteAsset']; const base = this.safeCurrencyCode (baseId); const quote = this.safeCurrencyCode (quoteId); const symbol = base + '/' + quote; const filters = this.indexBy (market['filters'], 'filterType'); const precision = { 'base': market['baseAssetPrecision'], 'quote': market['quotePrecision'], 'amount': market['baseAssetPrecision'], 'price': market['quotePrecision'], }; const status = this.safeString (market, 'status'); const active = (status === 'TRADING'); const entry = { 'id': id, 'lowercaseId': lowercaseId, 'symbol': symbol, 'base': base, 'quote': quote, 'baseId': baseId, 'quoteId': quoteId, 'info': market, 'type': marketType, 'spot': spot, 'future': future, 'active': active, 'precision': precision, 'limits': { 'amount': { 'min': Math.pow (10, -precision['amount']), 'max': undefined, }, 'price': { 'min': undefined, 'max': undefined, }, 'cost': { 'min': -1 * Math.log10 (precision['amount']), 'max': undefined, }, }, }; if ('PRICE_FILTER' in filters) { const filter = filters['PRICE_FILTER']; // PRICE_FILTER reports zero values for maxPrice // since they updated filter types in November 2018 // https://github.com/ccxt/ccxt/issues/4286 // therefore limits['price']['max'] doesn't have any meaningful value except undefined entry['limits']['price'] = { 'min': this.safeFloat (filter, 'minPrice'), 'max': undefined, }; const maxPrice = this.safeFloat (filter, 'maxPrice'); if ((maxPrice !== undefined) && (maxPrice > 0)) { entry['limits']['price']['max'] = maxPrice; } entry['precision']['price'] = this.precisionFromString (filter['tickSize']); } if ('LOT_SIZE' in filters) { const filter = this.safeValue (filters, 'LOT_SIZE', {}); const stepSize = this.safeString (filter, 'stepSize'); entry['precision']['amount'] = this.precisionFromString (stepSize); entry['limits']['amount'] = { 'min': this.safeFloat (filter, 'minQty'), 'max': this.safeFloat (filter, 'maxQty'), }; } if ('MARKET_LOT_SIZE' in filters) { const filter = this.safeValue (filters, 'MARKET_LOT_SIZE', {}); entry['limits']['market'] = { 'min': this.safeFloat (filter, 'minQty'), 'max': this.safeFloat (filter, 'maxQty'), }; } if ('MIN_NOTIONAL' in filters) { entry['limits']['cost']['min'] = this.safeFloat (filters['MIN_NOTIONAL'], 'minNotional'); } result.push (entry); } return result; } calculateFee (symbol, type, side, amount, price, takerOrMaker = 'taker', params = {}) { const market = this.markets[symbol]; let key = 'quote'; const rate = market[takerOrMaker]; let cost = amount * rate; let precision = market['precision']['price']; if (side === 'sell') { cost *= price; } else { key = 'base'; precision = market['precision']['amount']; } cost = this.decimalToPrecision (cost, ROUND, precision, this.precisionMode); return { 'type': takerOrMaker, 'currency': market[key], 'rate': rate, 'cost': parseFloat (cost), }; } async fetchBalance (params = {}) { await this.loadMarkets (); const defaultType = this.safeString2 (this.options, 'fetchBalance', 'defaultType', 'spot'); const type = this.safeString (params, 'type', defaultType); const method = (type === 'spot') ? 'privateGetAccount' : 'fapiPrivateGetAccount'; const query = this.omit (params, 'type'); const response = await this[method] (query); // // spot // // { // makerCommission: 10, // takerCommission: 10, // buyerCommission: 0, // sellerCommission: 0, // canTrade: true, // canWithdraw: true, // canDeposit: true, // updateTime: 1575357359602, // accountType: "MARGIN", // balances: [ // { asset: "BTC", free: "0.00219821", locked: "0.00000000" }, // ] // } // // futures (fapi) // // { // "feeTier":0, // "canTrade":true, // "canDeposit":true, // "canWithdraw":true, // "updateTime":0, // "totalInitialMargin":"0.00000000", // "totalMaintMargin":"0.00000000", // "totalWalletBalance":"4.54000000", // "totalUnrealizedProfit":"0.00000000", // "totalMarginBalance":"4.54000000", // "totalPositionInitialMargin":"0.00000000", // "totalOpenOrderInitialMargin":"0.00000000", // "maxWithdrawAmount":"4.54000000", // "assets":[ // { // "asset":"USDT", // "walletBalance":"4.54000000", // "unrealizedProfit":"0.00000000", // "marginBalance":"4.54000000", // "maintMargin":"0.00000000", // "initialMargin":"0.00000000", // "positionInitialMargin":"0.00000000", // "openOrderInitialMargin":"0.00000000", // "maxWithdrawAmount":"4.54000000" // } // ], // "positions":[ // { // "symbol":"BTCUSDT", // "initialMargin":"0.00000", // "maintMargin":"0.00000", // "unrealizedProfit":"0.00000000", // "positionInitialMargin":"0.00000", // "openOrderInitialMargin":"0.00000" // } // ] // } // const result = { 'info': response }; if (type === 'spot') { const balances = this.safeValue (response, 'balances', []); for (let i = 0; i < balances.length; i++) { const balance = balances[i]; const currencyId = this.safeString (balance, 'asset'); const code = this.safeCurrencyCode (currencyId); const account = this.account (); account['free'] = this.safeFloat (balance, 'free'); account['used'] = this.safeFloat (balance, 'locked'); result[code] = account; } } else { const balances = this.safeValue (response, 'assets', []); for (let i = 0; i < balances.length; i++) { const balance = balances[i]; const currencyId = this.safeString (balance, 'asset'); const code = this.safeCurrencyCode (currencyId); const account = this.account (); account['used'] = this.safeFloat (balance, 'initialMargin'); account['total'] = this.safeFloat (balance, 'marginBalance'); result[code] = account; } } return this.parseBalance (result); } async fetchOrderBook (symbol, limit = undefined, params = {}) { await this.loadMarkets (); const market = this.market (symbol); const request = { 'symbol': market['id'], }; if (limit !== undefined) { request['limit'] = limit; // default 100, max 5000, see https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#order-book } const method = market['spot'] ? 'publicGetDepth' : 'fapiPublicGetDepth'; const response = await this[method] (this.extend (request, params)); const orderbook = this.parseOrderBook (response); orderbook['nonce'] = this.safeInteger (response, 'lastUpdateId'); return orderbook; } parseTicker (ticker, market = undefined) { const timestamp = this.safeInteger (ticker, 'closeTime'); let symbol = undefined; const marketId = this.safeString (ticker, 'symbol'); if (marketId in this.markets_by_id) { market = this.markets_by_id[marketId]; } if (market !== undefined) { symbol = market['symbol']; } const last = this.safeFloat (ticker, 'lastPrice'); return { 'symbol': symbol, 'timestamp': timestamp, 'datetime': this.iso8601 (timestamp), 'high': this.safeFloat (ticker, 'highPrice'), 'low': this.safeFloat (ticker, 'lowPrice'), 'bid': this.safeFloat (ticker, 'bidPrice'), 'bidVolume': this.safeFloat (ticker, 'bidQty'), 'ask': this.safeFloat (ticker, 'askPrice'), 'askVolume': this.safeFloat (ticker, 'askQty'), 'vwap': this.safeFloat (ticker, 'weightedAvgPrice'), 'open': this.safeFloat (ticker, 'openPrice'), 'close': last, 'last': last, 'previousClose': this.safeFloat (ticker, 'prevClosePrice'), // previous day close 'change': this.safeFloat (ticker, 'priceChange'), 'percentage': this.safeFloat (ticker, 'priceChangePercent'), 'average': undefined, 'baseVolume': this.safeFloat (ticker, 'volume'), 'quoteVolume': this.safeFloat (ticker, 'quoteVolume'), 'info': ticker, }; } async fetchStatus (params = {}) { const response = await this.wapiGetSystemStatus (params); let status = this.safeValue (response, 'status'); if (status !== undefined) { status = (status === 0) ? 'ok' : 'maintenance'; this.status = this.extend (this.status, { 'status': status, 'updated': this.milliseconds (), }); } return this.status; } async fetchTicker (symbol, params = {}) { await this.loadMarkets (); const market = this.market (symbol); const request = { 'symbol': market['id'], }; const method = market['spot'] ? 'publicGetTicker24hr' : 'fapiPublicGetTicker24hr'; const response = await this[method] (this.extend (request, params)); return this.parseTicker (response, market); } parseTickers (rawTickers, symbols = undefined) { const tickers = []; for (let i = 0; i < rawTickers.length; i++) { tickers.push (this.parseTicker (rawTickers[i])); } return this.filterByArray (tickers, 'symbol', symbols); } async fetchBidsAsks (symbols = undefined, params = {}) { await this.loadMarkets (); const defaultType = this.safeString2 (this.options, 'fetchOpenOrders', 'defaultType', 'spot'); const type = this.safeString (params, 'type', defaultType); const query = this.omit (params, 'type'); const method = (type === 'spot') ? 'publicGetTickerBookTicker' : 'fapiPublicGetTickerBookTicker'; const response = await this[method] (query); return this.parseTickers (response, symbols); } async fetchTickers (symbols = undefined, params = {}) { await this.loadMarkets (); const method = this.options['fetchTickersMethod']; const response = await this[method] (params); return this.parseTickers (response, symbols); } parseOHLCV (ohlcv, market = undefined, timeframe = '1m', since = undefined, limit = undefined) { return [ ohlcv[0], parseFloat (ohlcv[1]), parseFloat (ohlcv[2]), parseFloat (ohlcv[3]), parseFloat (ohlcv[4]), parseFloat (ohlcv[5]), ]; } async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) { await this.loadMarkets (); const market = this.market (symbol); const request = { 'symbol': market['id'], 'interval': this.timeframes[timeframe], }; if (since !== undefined) { request['startTime'] = since; } if (limit !== undefined) { request['limit'] = limit; // default == max == 500 } const method = market['spot'] ? 'publicGetKlines' : 'fapiPublicGetKlines'; const response = await this[method] (this.extend (request, params)); return this.parseOHLCVs (response, market, timeframe, since, limit); } parseTrade (trade, market = undefined) { if ('isDustTrade' in trade) { return this.parseDustTrade (trade, market); } // // aggregate trades // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list // // { // "a": 26129, // Aggregate tradeId // "p": "0.01633102", // Price // "q": "4.70443515", // Quantity // "f": 27781, // First tradeId // "l": 27781, // Last tradeId // "T": 1498793709153, // Timestamp // "m": true, // Was the buyer the maker? // "M": true // Was the trade the best price match? // } // // recent public trades and old public trades // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#old-trade-lookup-market_data // // { // "id": 28457, // "price": "4.00000100", // "qty": "12.00000000", // "time": 1499865549590, // "isBuyerMaker": true, // "isBestMatch": true // } // // private trades // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-trade-list-user_data // // { // "symbol": "BNBBTC", // "id": 28457, // "orderId": 100234, // "price": "4.00000100", // "qty": "12.00000000", // "commission": "10.10000000", // "commissionAsset": "BNB", // "time": 1499865549590, // "isBuyer": true, // "isMaker": false, // "isBestMatch": true // } // // futures trades // https://binance-docs.github.io/apidocs/futures/en/#account-trade-list-user_data // // { // "accountId": 20, // "buyer": False, // "commission": "-0.07819010", // "commissionAsset": "USDT", // "counterPartyId": 653, // "id": 698759, // "maker": False, // "orderId": 25851813, // "price": "7819.01", // "qty": "0.002", // "quoteQty": "0.01563", // "realizedPnl": "-0.91539999", // "side": "SELL", // "symbol": "BTCUSDT", // "time": 1569514978020 // } // const timestamp = this.safeInteger2 (trade, 'T', 'time'); const price = this.safeFloat2 (trade, 'p', 'price'); const amount = this.safeFloat2 (trade, 'q', 'qty'); const id = this.safeString2 (trade, 'a', 'id'); let side = undefined; const orderId = this.safeString (trade, 'orderId'); if ('m' in trade) { side = trade['m'] ? 'sell' : 'buy'; // this is reversed intentionally } else if ('isBuyerMaker' in trade) { side = trade['isBuyerMaker'] ? 'sell' : 'buy'; } else if ('side' in trade) { side = this.safeStringLower (trade, 'side'); } else { if ('isBuyer' in trade) { side = trade['isBuyer'] ? 'buy' : 'sell'; // this is a true side } } let fee = undefined; if ('commission' in trade) { fee = { 'cost': this.safeFloat (trade, 'commission'), 'currency': this.safeCurrencyCode (this.safeString (trade, 'commissionAsset')), }; } let takerOrMaker = undefined; if ('isMaker' in trade) { takerOrMaker = trade['isMaker'] ? 'maker' : 'taker'; } let symbol = undefined; if (market === undefined) { const marketId = this.safeString (trade, 'symbol'); market = this.safeValue (this.markets_by_id, marketId); } if (market !== undefined) { symbol = market['symbol']; } return { 'info': trade, 'timestamp': timestamp, 'datetime': this.iso8601 (timestamp), 'symbol': symbol, 'id': id, 'order': orderId, 'type': undefined, 'takerOrMaker': takerOrMaker, 'side': side, 'price': price, 'amount': amount, 'cost': price * amount, 'fee': fee, }; } async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) { await this.loadMarkets (); const market = this.market (symbol); const request = { 'symbol': market['id'], // 'fromId': 123, // ID to get aggregate trades from INCLUSIVE. // 'startTime': 456, // Timestamp in ms to get aggregate trades from INCLUSIVE. // 'endTime': 789, // Timestamp in ms to get aggregate trades until INCLUSIVE. // 'limit': 500, // default = 500, maximum = 1000 }; const defaultType = this.safeString2 (this.options, 'fetchTrades', 'defaultType', 'spot'); const type = this.safeString (params, 'type', defaultType); const query = this.omit (params, 'type'); const defaultMethod = (type === 'future') ? 'fapiPublicGetTrades' : 'publicGetTrades'; let method = this.safeString (this.options, 'fetchTradesMethod', defaultMethod); if (method === 'publicGetAggTrades') { if (since !== undefined) { request['startTime'] = since; // https://github.com/ccxt/ccxt/issues/6400 // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list request['endTime'] = this.sum (since, 3600000); } if (type === 'future') { method = 'fapiPublicGetAggTrades'; } } else if ((method === 'publicGetHistoricalTrades') && (type === 'future')) { method = 'fapiPublicGetHistoricalTrades'; } if (limit !== undefined) { request['limit'] = limit; // default = 500, maximum = 1000 } // // Caveats: // - default limit (500) applies only if no other parameters set, trades up // to the maximum limit may be returned to satisfy other parameters // - if both limit and time window is set and time window contains more // trades than the limit then the last trades from the window are returned // - 'tradeId' accepted and returned by this method is "aggregate" trade id // which is different from actual trade id // - setting both fromId and time window results in error const response = await this[method] (this.extend (request, query)); // // aggregate trades // // [ // { // "a": 26129, // Aggregate tradeId // "p": "0.01633102", // Price // "q": "4.70443515", // Quantity // "f": 27781, // First tradeId // "l": 27781, // Last tradeId // "T": 1498793709153, // Timestamp // "m": true, // Was the buyer the maker? // "M": true // Was the trade the best price match? // } // ] // // recent public trades and historical public trades // // [ // { // "id": 28457, // "price": "4.00000100", // "qty": "12.00000000", // "time": 1499865549590, // "isBuyerMaker": true, // "isBestMatch": true // } // ] // return this.parseTrades (response, market, since, limit); } parseOrderStatus (status) { const statuses = { 'NEW': 'open', 'PARTIALLY_FILLED': 'open', 'FILLED': 'closed', 'CANCELED': 'canceled', 'PENDING_CANCEL': 'canceling', // currently unused 'REJECTED': 'rejected', 'EXPIRED': 'canceled', }; return this.safeString (statuses, status, status); } parseOrder (order, market = undefined) { // // spot // // { // "symbol": "LTCBTC", // "orderId": 1, // "clientOrderId": "myOrder1", // "price": "0.1", // "origQty": "1.0", // "executedQty": "0.0", // "cummulativeQuoteQty": "0.0", // "status": "NEW", // "timeInForce": "GTC", // "type": "LIMIT", // "side": "BUY", // "stopPrice": "0.0", // "icebergQty": "0.0", // "time": 1499827319559, // "updateTime": 1499827319559, // "isWorking": true // } // // futures // // { // "symbol": "BTCUSDT", // "orderId": 1, // "clientOrderId": "myOrder1", // "price": "0.1", // "origQty": "1.0", // "executedQty": "1.0", // "cumQuote": "10.0", // "status": "NEW", // "timeInForce": "GTC", // "type": "LIMIT", // "side": "BUY", // "stopPrice": "0.0", // "updateTime": 1499827319559 // } // const status = this.parseOrderStatus (this.safeString (order, 'status')); let symbol = undefined; const marketId = this.safeString (order, 'symbol'); if (marketId in this.markets_by_id) { market = this.markets_by_id[marketId]; } if (market !== undefined) { symbol = market['symbol']; } let timestamp = undefined; if ('time' in order) { timestamp = this.safeInteger (order, 'time'); } else if ('transactTime' in order) { timestamp = this.safeInteger (order, 'transactTime'); } let price = this.safeFloat (order, 'price'); const amount = this.safeFloat (order, 'origQty'); const filled = this.safeFloat (order, 'executedQty'); let remaining = undefined; // - Spot/Margin market: cummulativeQuoteQty // - Futures market: cumQuote. // Note this is not the actual cost, since Binance futures uses leverage to calculate margins. let cost = this.safeFloat2 (order, 'cummulativeQuoteQty', 'cumQuote'); if (filled !== undefined) { if (amount !== undefined) { remaining = amount - filled; if (this.options['parseOrderToPrecision']) { remaining = parseFloat (this.amountToPrecision (symbol, remaining)); } remaining = Math.max (remaining, 0.0); } if (price !== undefined) { if (cost === undefined) { cost = price * filled; } } } const id = this.safeString (order, 'orderId'); let type = this.safeStringLower (order, 'type'); if (type === 'market') { if (price === 0.0) { if ((cost !== undefined) && (filled !== undefined)) { if ((cost > 0) && (filled > 0)) { price = cost / filled; if (this.options['parseOrderToPrecision']) { price = parseFloat (this.priceToPrecision (symbol, price)); } } } } } else if (type === 'limit_maker') { type = 'limit'; } const side = this.safeStringLower (order, 'side'); let fee = undefined; let trades = undefined; const fills = this.safeValue (order, 'fills'); if (fills !== undefined) { trades = this.parseTrades (fills, market); const numTrades = trades.length; if (numTrades > 0) { cost = trades[0]['cost']; fee = { 'cost': trades[0]['fee']['cost'], 'currency': trades[0]['fee']['currency'], }; for (let i = 1; i < trades.length; i++) { cost = this.sum (cost, trades[i]['cost']); fee['cost'] = this.sum (fee['cost'], trades[i]['fee']['cost']); } } } let average = undefined; if (cost !== undefined) { if (filled) { average = cost / filled; if (this.options['parseOrderToPrecision']) { average = parseFloat (this.priceToPrecision (symbol, average)); } } if (this.options['parseOrderToPrecision']) { cost = parseFloat (this.costToPrecision (symbol, cost)); } } const clientOrderId = this.safeString (order, 'clientOrderId'); return { 'info': order, 'id': id, 'clientOrderId': clientOrderId, 'timestamp': timestamp, 'datetime': this.iso8601 (timestamp), 'lastTradeTimestamp': undefined, 'symbol': symbol, 'type': type, 'side': side, 'price': price, 'amount': amount, 'cost': cost, 'average': average, 'filled': filled, 'remaining': remaining, 'status': status, 'fee': fee, 'trades': trades, }; } async createOrder (symbol, type, side, amount, price = undefined, params = {}) { await this.loadMarkets (); const market = this.market (symbol); // the next 5 lines are added to support for testing orders let method = market['spot'] ? 'privatePostOrder' : 'fapiPrivatePostOrder'; if (market['spot']) { const test = this.safeValue (params, 'test', false); if (test) { method += 'Test'; params = this.omit (params, 'test'); } } const uppercaseType = type.toUpperCase (); const validOrderTypes = this.safeValue (market['info'], 'orderTypes'); if (!this.inArray (uppercaseType, validOrderTypes)) { throw new InvalidOrder (this.id + ' ' + type + ' is not a valid order type in ' + market['type'] + ' market ' + symbol); } const request = { 'symbol': market['id'], 'type': uppercaseType, 'side': side.toUpperCase (), }; if (uppercaseType === 'MARKET') { const quoteOrderQty = this.safeFloat (params, 'quoteOrderQty'); const precision = market['precision']['price']; if (quoteOrderQty !== undefined) { request['quoteOrderQty'] = this.decimalToPrecision (quoteOrderQty, TRUNCATE, precision, this.precisionMode); params = this.omit (params, 'quoteOrderQty'); } else if (price !== undefined) { request['quoteOrderQty'] = this.decimalToPrecision (amount * price, TRUNCATE, precision, this.precisionMode); } else { request['quantity'] = this.amountToPrecision (symbol, amount); } } else { request['quantity'] = this.amountToPrecision (symbol, amount); } if (market['spot']) { request['newOrderRespType'] = this.safeValue (this.options['newOrderRespType'], type, 'RESULT'); // 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills } let timeInForceIsRequired = false; let priceIsRequired = false; let stopPriceIsRequired = false; if (uppercaseType === 'LIMIT') { priceIsRequired = true; timeInForceIsRequired = true; } else if ((uppercaseType === 'STOP_LOSS') || (uppercaseType === 'TAKE_PROFIT')) { stopPriceIsRequired = true; if (market['future']) { priceIsRequired = true; } } else if ((uppercaseType === 'STOP_LOSS_LIMIT') || (uppercaseType === 'TAKE_PROFIT_LIMIT')) { stopPriceIsRequired = true; priceIsRequired = true; timeInForceIsRequired = true; } else if (uppercaseType === 'LIMIT_MAKER') { priceIsRequired = true; } else if (uppercaseType === 'STOP') { stopPriceIsRequired = true; priceIsRequired = true; } if (priceIsRequired) { if (price === undefined) { throw new InvalidOrder (this.id + ' createOrder method requires a price argument for a ' + type + ' order'); } request['price'] = this.priceToPrecision (symbol, price); } if (timeInForceIsRequired) { request['timeInForce'] = this.options['defaultTimeInForce']; // 'GTC' = Good To Cancel (default), 'IOC' = Immediate Or Cancel } if (stopPriceIsRequired) { const stopPrice = this.safeFloat (params, 'stopPrice'); if (stopPrice === undefined) { throw new InvalidOrder (this.id + ' createOrder method requires a stopPrice extra param for a ' + type + ' order'); } else { params = this.omit (params, 'stopPrice'); request['stopPrice'] = this.priceToPrecision (symbol, stopPrice); } } const response = await this[method] (this.extend (request, params)); return this.parseOrder (response, market); } async fetchOrder (id, symbol = undefined, params = {}) { if (symbol === undefined) { throw new ArgumentsRequired (this.id + ' fetchOrder requires a symbol argument'); } await this.loadMarkets (); const market = this.market (symbol); const method = market['spot'] ? 'privateGetOrder' : 'fapiPrivateGetOrder'; const request = { 'symbol': market['id'], }; const origClientOrderId = this.safeValue (params, 'origClientOrderId'); if (origClientOrderId !== undefined) { request['origClientOrderId'] = origClientOrderId; } else { request['orderId'] = parseInt (id); } const response = await this[method] (this.extend (request, params)); return this.parseOrder (response, market); } async fetchOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) { if (symbol === undefined) { throw new ArgumentsRequired (this.id + ' fetchOrders requires a symbol argument'); } await this.loadMarkets (); const market = this.market (symbol); const request = { 'symbol': market['id'], }; if (since !== undefined) { request['startTime'] = since; } if (limit !== undefined) { request['limit'] = limit; } const method = market['spot'] ? 'privateGetAllOrders' : 'fapiPrivateGetAllOrders'; const response = await this[method] (this.extend (request, params)); // // Spot: // [ // { // "symbol": "LTCBTC", // "orderId": 1, // "clientOrderId": "myOrder1", // "price": "0.1", // "origQty": "1.0", // "executedQty": "0.0", // "cummulativeQuoteQty": "0.0", // "status": "NEW", // "timeInForce": "GTC", // "type": "LIMIT", // "side": "BUY", // "stopPrice": "0.0", // "icebergQty": "0.0", // "time": 1499827319559, // "updateTime": 1499827319559, // "isWorking": true // } // ] // // Futures: // [ // { // "symbol": "BTCUSDT", // "orderId": 1, // "clientOrderId": "myOrder1", // "price": "0.1", // "origQty": "1.0", // "executedQty": "1.0", // "cumQuote": "10.0", // "status": "NEW", // "timeInForce": "GTC", // "type": "LIMIT", // "side": "BUY", // "stopPrice": "0.0", // "updateTime": 1499827319559 // } // ] // return this.parseOrders (response, market, since, limit); } async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) { await this.loadMarkets (); let market = undefined; let query = undefined; let type = undefined; const request = {}; if (symbol !== undefined) { market = this.market (symbol); request['symbol'] = market['id']; type = market['type']; query = params; } else if (this.options['warnOnFetchOpenOrdersWithoutSymbol']) { const symbols = this.symbols; const numSymbols = symbols.length; const fetchOpenOrdersRateLimit = parseInt (numSymbols / 2); throw new ExchangeError (this.id + ' fetchOpenOrders WARNING: fetching open orders without specifying a symbol is rate-limited to one call per ' + fetchOpenOrdersRateLimit.toString () + ' seconds. Do not call this method frequently to avoid ban. Set ' + this.id + '.options["warnOnFetchOpenOrdersWithoutSymbol"] = false to suppress this warning message.'); } else { const defaultType = this.safeString2 (this.options, 'fetchOpenOrders', 'defaultType', 'spot'); type = this.safeString (params, 'type', defaultType); query = this.omit (params, 'type'); } const method = (type === 'spot') ? 'privateGetOpenOrders' : 'fapiPrivateGetOpenOrders'; const response = await this[method] (this.extend (request, query)); return this.parseOrders (response, market, since, limit); } async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) { const orders = await this.fetchOrders (symbol, since, limit, params); return this.filterBy (orders, 'status', 'closed'); } async cancelOrder (id, symbol = undefined, params = {}) { if (symbol === undefined) { throw new ArgumentsRequired (this.id + ' cancelOrder requires a symbol argument'); } await this.loadMarkets (); const market = this.market (symbol); // https://github.com/ccxt/ccxt/issues/6507 const origClientOrderId = this.safeValue (params, 'origClientOrderId'); const request = { 'symbol': market['id'], // 'orderId': parseInt (id), // 'origClientOrderId': id, }; if (origClientOrderId === undefined) { request['orderId'] = parseInt (id); } else { request['origClientOrderId'] = origClientOrderId; } const method = market['spot'] ? 'privateDeleteOrder' : 'fapiPrivateDeleteOrder'; const response = await this[method] (this.extend (request, params)); return this.parseOrder (response); } async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) { if (symbol === undefined) { throw new ArgumentsRequired (this.id + ' fetchMyTrades requires a symbol argument'); } await this.loadMarkets (); const market = this.market (symbol); const method = market['spot'] ? 'privateGetMyTrades' : 'fapiPrivateGetUserTrades'; const request = { 'symbol': market['id'], }; if (since !== undefined) { request['startTime'] = since; } if (limit !== undefined) { request['limit'] = limit; } const response = await this[method] (this.extend (request, params)); // // spot trade // [ // { // "symbol": "BNBBTC", // "id": 28457, // "orderId": 100234, // "price": "4.00000100", // "qty": "12.00000000", // "commission": "10.10000000", // "commissionAsset": "BNB", // "time": 1499865549590, // "isBuyer": true, // "isMaker": false, // "isBestMatch": true, // } // ] // // futures trade // // [ // { // "accountId": 20, // "buyer": False, // "commission": "-0.07819010", // "commissionAsset": "USDT", // "counterPartyId": 653, // "id": 698759, // "maker": False, // "orderId": 25851813, // "price": "7819.01", // "qty": "0.002", // "quoteQty": "0.01563", // "realizedPnl": "-0.91539999", // "side": "SELL", // "symbol": "BTCUSDT", // "time": 1569514978020 // } // ] return this.parseTrades (response, market, since, limit); } async fetchMyDustTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) { // // Binance provides an opportunity to trade insignificant (i.e. non-tradable and non-withdrawable) // token leftovers (of any asset) into `BNB` coin which in turn can be used to pay trading fees with it. // The corresponding trades history is called the `Dust Log` and can be requested via the following end-point: // https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#dustlog-user_data // await this.loadMarkets (); const response = await this.wapiGetUserAssetDribbletLog (params); // { success: true, // results: { total: 1, // rows: [ { transfered_total: "1.06468458", // service_charge_total: "0.02172826", // tran_id: 2701371634, // logs: [ { tranId: 2701371634, // serviceChargeAmount: "0.00012819", // uid: "35103861", // amount: "0.8012", // operateTime: "2018-10-07 17:56:07", // transferedAmount: "0.00628141", // fromAsset: "ADA" } ], // operate_time: "2018-10-07 17:56:06" } ] } } const results = this.safeValue (response, 'results', {}); const rows = this.safeValue (results, 'rows', []); const data = []; for (let i = 0; i < rows.length; i++) { const logs = rows[i]['logs']; for (let j = 0; j < logs.length; j++) { logs[j]['isDustTrade'] = true; data.push (logs[j]); } } const trades = this.parseTrades (data, undefined, since, limit); return this.filterBySinceLimit (trades, since, limit); } parseDustTrade (trade, market = undefined) { // { tranId: 2701371634, // serviceChargeAmount: "0.00012819", // uid: "35103861", // amount: "0.8012", // operateTime: "2018-10-07 17:56:07", // transferedAmount: "0.00628141", // fromAsset: "ADA" }, const orderId = this.safeString (trade, 'tranId'); const timestamp = this.parse8601 (this.safeString (trade, 'operateTime')); const tradedCurrency = this.safeCurrencyCode (this.safeString (trade, 'fromAsset')); const earnedCurrency = this.currency ('BNB')['code']; const applicantSymbol = earnedCurrency + '/' + tradedCurrency; let tradedCurrencyIsQuote = false; if (applicantSymbol in this.markets) { tradedCurrencyIsQuote = true; } // // Warning // Binance dust trade `fee` is already excluded from the `BNB` earning reported in the `Dust Log`. // So the parser should either set the `fee.cost` to `0` or add it on top of the earned // BNB `amount` (or `cost` depending on the trade `side`). The second of the above options // is much more illustrative and therefore preferable. // const fee = { 'currency': earnedCurrency, 'cost': this.safeFloat (trade, 'serviceChargeAmount'), }; let symbol = undefined; let amount = undefined; let cost = undefined; let side = undefined; if (tradedCurrencyIsQuote) { symbol = applicantSymbol; amount = this.sum (this.safeFloat (trade, 'transferedAmount'), fee['cost']); cost = this.safeFloat (trade, 'amount'); side = 'buy'; } else { symbol = tradedCurrency + '/' + earnedCurrency; amount = this.safeFloat (trade, 'amount'); cost = this.sum (this.safeFloat (trade, 'transferedAmount'), fee['cost']); side = 'sell'; } let price = undefined; if (cost !== undefined) { if (amount) { price = cost / amount; } } const id = undefined; const type = undefined; const takerOrMaker = undefined; return { 'id': id, 'timestamp': timestamp, 'datetime': this.iso8601 (timestamp), 'symbol': symbol, 'order': orderId, 'type': type, 'takerOrMaker': takerOrMaker, 'side': side, 'amount': amount, 'price': price, 'cost': cost, 'fee': fee, 'info': trade, }; } async fetchDeposits (code = undefined, since = undefined, limit = undefined, params = {}) { await this.loadMarkets (); let currency = undefined; const request = {}; if (code !== undefined) { currency = this.currency (code); request['asset'] = currency['id']; } if (since !== undefined) { request['startTime'] = since; // max 3 months range https://github.com/ccxt/ccxt/issues/6495 request['endTime'] = this.sum (since, 7776000000); } const response = await this.wapiGetDepositHistory (this.extend (request, params)); // // { success: true, // depositList: [ { insertTime: 1517425007000, // amount: 0.3, // address: "0x0123456789abcdef", // addressTag: "", // txId: "0x0123456789abcdef", // asset: "ETH", // status: 1 } ] } // return this.parseTransactions (response['depositList'], currency, since, limit); } async fetchWithdrawals (code = undefined, since = undefined, limit = undefined, params = {}) { await this.loadMarkets (); let currency = undefined; const request = {}; if (code !== undefined) { currency = this.currency (code); request['asset'] = currency['id']; } if (since !== undefined) { request['startTime'] = since; // max 3 months range https://github.com/ccxt/ccxt/issues/6495 request['endTime'] = this.sum (since, 7776000000); } const response = await this.wapiGetWithdrawHistory (this.extend (request, params)); // // { withdrawList: [ { amount: 14, // address: "0x0123456789abcdef...", // successTime: 1514489710000, // transactionFee: 0.01, // addressTag: "", // txId: "0x0123456789abcdef...", // id: "0123456789abcdef...", // asset: "ETH", // applyTime: 1514488724000, // status: 6 }, // { amount: 7600, // address: "0x0123456789abcdef...", // successTime: 1515323226000, // transactionFee: 0.01, // addressTag: "", // txId: "0x0123456789abcdef...", // id: "0123456789abcdef...", // asset: "ICN", // applyTime: 1515322539000, // status: 6 } ], // success: true } // return this.parseTransactions (response['withdrawList'], currency, since, limit); } parseTransactionStatusByType (status, type = undefined) { if (type === undefined) { return status; } const statuses = { 'deposit': { '0': 'pending', '1': 'ok', }, 'withdrawal': { '0': 'pending', // Email Sent '1': 'canceled', // Cancelled (different from 1 = ok in deposits) '2': 'pending', // Awaiting Approval '3': 'failed', // Rejected '4': 'pending', // Processing '5': 'failed', // Failure '6': 'ok', // Completed }, }; return (status in statuses[type]) ? statuses[type][status] : status; } parseTransaction (transaction, currency = undefined) { // // fetchDeposits // { insertTime: 1517425007000, // amount: 0.3, // address: "0x0123456789abcdef", // addressTag: "", // txId: "0x0123456789abcdef", // asset: "ETH", // status: 1 } // // fetchWithdrawals // // { amount: 14, // address: "0x0123456789abcdef...", // successTime: 1514489710000, // transactionFee: 0.01, // addressTag: "", // txId: "0x0123456789abcdef...", // id: "0123456789abcdef...", // asset: "ETH", // applyTime: 1514488724000, // status: 6 } // const id = this.safeString (transaction, 'id'); const address = this.safeString (transaction, 'address'); let tag = this.safeString (transaction, 'addressTag'); // set but unused if (tag !== undefined) { if (tag.length < 1) { tag = undefined; } } const txid = this.safeValue (transaction, 'txId'); const currencyId = this.safeString (transaction, 'asset'); const code = this.safeCurrencyCode (currencyId, currency); let timestamp = undefined; const insertTime = this.safeInteger (transaction, 'insertTime'); const applyTime = this.safeInteger (transaction, 'applyTime'); let type = this.safeString (transaction, 'type'); if (type === undefined) { if ((insertTime !== undefined) && (applyTime === undefined)) { type = 'deposit'; timestamp = insertTime; } else if ((insertTime === undefined) && (applyTime !== undefined)) { type = 'withdrawal'; timestamp = applyTime; } } const status = this.parseTransactionStatusByType (this.safeString (transaction, 'status'), type); const amount = this.safeFloat (transaction, 'amount'); const feeCost = this.safeFloat (transaction, 'transactionFee'); let fee = undefined; if (feeCost !== undefined) { fee = { 'currency': code, 'cost': feeCost }; } return { 'info': transaction, 'id': id, 'txid': txid, 'timestamp': timestamp, 'datetime': this.iso8601 (timestamp), 'address': address, 'tag': tag, 'type': type, 'amount': amount, 'currency': code, 'status': status, 'updated': undefined, 'fee': fee, }; } async fetchDepositAddress (code, params = {}) { await this.loadMarkets (); const currency = this.currency (code); const request = { 'asset': currency['id'], }; const response = await this.wapiGetDepositAddress (this.extend (request, params)); const success = this.safeValue (response, 'success'); if ((success === undefined) || !success) { throw new InvalidAddress (this.id + ' fetchDepositAddress returned an empty response – create the deposit address in the user settings first.'); } const address = this.safeString (response, 'address'); const tag = this.safeString (response, 'addressTag'); this.checkAddress (address); return { 'currency': code, 'address': this.checkAddress (address), 'tag': tag, 'info': response, }; } async fetchFundingFees (codes = undefined, params = {}) { const response = await this.wapiGetAssetDetail (params); // // { // "success": true, // "assetDetail": { // "CTR": { // "minWithdrawAmount": "70.00000000", //min withdraw amount // "depositStatus": false,//deposit status // "withdrawFee": 35, // withdraw fee // "withdrawStatus": true, //withdraw status // "depositTip": "Delisted, Deposit Suspended" //reason // }, // "SKY": { // "minWithdrawAmount": "0.02000000", // "depositStatus": true, // "withdrawFee": 0.01, // "withdrawStatus": true // } // } // } // const detail = this.safeValue (response, 'assetDetail', {}); const ids = Object.keys (detail); const withdrawFees = {}; for (let i = 0; i < ids.length; i++) { const id = ids[i]; const code = this.safeCurrencyCode (id); withdrawFees[code] = this.safeFloat (detail[id], 'withdrawFee'); } return { 'withdraw': withdrawFees, 'deposit': {}, 'info': response, }; } async withdraw (code, amount, address, tag = undefined, params = {}) { this.checkAddress (address); await this.loadMarkets (); const currency = this.currency (code); // name is optional, can be overrided via params const name = address.slice (0, 20); const request = { 'asset': currency['id'], 'address': address, 'amount': parseFloat (amount), 'name': name, // name is optional, can be overrided via params // https://binance-docs.github.io/apidocs/spot/en/#withdraw-sapi // issue sapiGetCapitalConfigGetall () to get networks for withdrawing USDT ERC20 vs USDT Omni // 'network': 'ETH', // 'BTC', 'TRX', etc, optional }; if (tag !== undefined) { request['addressTag'] = tag; } const response = await this.wapiPostWithdraw (this.extend (request, params)); return { 'info': response, 'id': this.safeString (response, 'id'), }; } parseTradingFee (fee, market = undefined) { // // { // "symbol": "ADABNB", // "maker": 0.9000, // "taker": 1.0000 // } // const marketId = this.safeString (fee, 'symbol'); let symbol = marketId; if (marketId in this.markets_by_id) { const market = this.markets_by_id[marketId]; symbol = market['symbol']; } return { 'info': fee, 'symbol': symbol, 'maker': this.safeFloat (fee, 'maker'), 'taker': this.safeFloat (fee, 'taker'), }; } async fetchTradingFee (symbol, params = {}) { await this.loadMarkets (); const market = this.market (symbol); const request = { 'symbol': market['id'], }; const response = await this.wapiGetTradeFee (this.extend (request, params)); // // { // "tradeFee": [ // { // "symbol": "ADABNB", // "maker": 0.9000, // "taker": 1.0000 // } // ], // "success": true // } // const tradeFee = this.safeValue (response, 'tradeFee', []); const first = this.safeValue (tradeFee, 0, {}); return this.parseTradingFee (first); } async fetchTradingFees (params = {}) { await this.loadMarkets (); const response = await this.wapiGetTradeFee (params); // // { // "tradeFee": [ // { // "symbol": "ADABNB", // "maker": 0.9000, // "taker": 1.0000 // } // ], // "success": true // } // const tradeFee = this.safeValue (response, 'tradeFee', []); const result = {}; for (let i = 0; i < tradeFee.length; i++) { const fee = this.parseTradingFee (tradeFee[i]); const symbol = fee['symbol']; result[symbol] = fee; } return result; } sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { let url = this.urls['api'][api]; url += '/' + path; if (api === 'wapi') { url += '.html'; } const userDataStream = (path === 'userDataStream') || (path === 'listenKey'); if (path === 'historicalTrades') { if (this.apiKey) { headers = { 'X-MBX-APIKEY': this.apiKey, }; } else { throw new AuthenticationError (this.id + ' historicalTrades endpoint requires `apiKey` credential'); } } else if (userDataStream) { if (this.apiKey) { // v1 special case for userDataStream body = this.urlencode (params); headers = { 'X-MBX-APIKEY': this.apiKey, 'Content-Type': 'application/x-www-form-urlencoded', }; } else { throw new AuthenticationError (this.id + ' userDataStream endpoint requires `apiKey` credential'); } } if ((api === 'private') || (api === 'sapi') || (api === 'wapi' && path !== 'systemStatus') || (api === 'fapiPrivate')) { this.checkRequiredCredentials (); let query = undefined; if ((api === 'sapi') && (path === 'asset/dust')) { query = this.urlencodeWithArrayRepeat (this.extend ({ 'timestamp': this.nonce (), 'recvWindow': this.options['recvWindow'], }, params)); } else { query = this.urlencode (this.extend ({ 'timestamp': this.nonce (), 'recvWindow': this.options['recvWindow'], }, params)); } const signature = this.hmac (this.encode (query), this.encode (this.secret)); query += '&' + 'signature=' + signature; headers = { 'X-MBX-APIKEY': this.apiKey, }; if ((method === 'GET') || (method === 'DELETE') || (api === 'wapi')) { url += '?' + query; } else { body = query; headers['Content-Type'] = 'application/x-www-form-urlencoded'; } } else { // userDataStream endpoints are public, but POST, PUT, DELETE // therefore they don't accept URL query arguments // https://github.com/ccxt/ccxt/issues/5224 if (!userDataStream) { if (Object.keys (params).length) { url += '?' + this.urlencode (params); } } } return { 'url': url, 'method': method, 'body': body, 'headers': headers }; } handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) { if ((code === 418) || (code === 429)) { throw new DDoSProtection (this.id + ' ' + code.toString () + ' ' + reason + ' ' + body); } // error response in a form: { "code": -1013, "msg": "Invalid quantity." } // following block cointains legacy checks against message patterns in "msg" property // will switch "code" checks eventually, when we know all of them if (code >= 400) { if (body.indexOf ('Price * QTY is zero or less') >= 0) { throw new InvalidOrder (this.id + ' order cost = amount * price is zero or less ' + body); } if (body.indexOf ('LOT_SIZE') >= 0) { throw new InvalidOrder (this.id + ' order amount should be evenly divisible by lot size ' + body); } if (body.indexOf ('PRICE_FILTER') >= 0) { throw new InvalidOrder (this.id + ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid float value in general, use this.priceToPrecision (symbol, amount) ' + body); } } if (body.length > 0) { if (body[0] === '{') { // check success value for wapi endpoints // response in format {'msg': 'The coin does not exist.', 'success': true/false} const success = this.safeValue (response, 'success', true); if (!success) { const message = this.safeString (response, 'msg'); let parsedMessage = undefined; if (message !== undefined) { try { parsedMessage = JSON.parse (message); } catch (e) { // do nothing parsedMessage = undefined; } if (parsedMessage !== undefined) { response = parsedMessage; } } } const message = this.safeString (response, 'msg'); if (message !== undefined) { this.throwExactlyMatchedException (this.exceptions, message, this.id + ' ' + message); } // checks against error codes const error = this.safeString (response, 'code'); if (error !== undefined) { // https://github.com/ccxt/ccxt/issues/6501 if (error === '200') { return; } // a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} // despite that their message is very confusing, it is raised by Binance // on a temporary ban, the API key is valid, but disabled for a while if ((error === '-2015') && this.options['hasAlreadyAuthenticatedSuccessfully']) { throw new DDoSProtection (this.id + ' temporary banned: ' + body); } const feedback = this.id + ' ' + body; this.throwExactlyMatchedException (this.exceptions, error, feedback); throw new ExchangeError (feedback); } if (!success) { throw new ExchangeError (this.id + ' ' + body); } } } } async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { const response = await this.fetch2 (path, api, method, params, headers, body); // a workaround for {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} if ((api === 'private') || (api === 'wapi')) { this.options['hasAlreadyAuthenticatedSuccessfully'] = true; } return response; } };