websocket-crypto-api
Version:
582 lines (541 loc) • 17.6 kB
JavaScript
/* eslint-disable max-len,class-methods-use-this */
import CryptoJS from 'crypto-js';
import ReWS from 'reconnecting-websocket';
import { Decimal } from 'decimal.js';
import Exchanges from './baseExchange';
const RecvWindow = 60000;
export default class binanceDEX extends Exchanges {
constructor(data = {}) {
super();
this.name = 'Binance';
this._mainUrl = 'wss://testnet-dex.binance.org/api/ws/';
this._sockets = {};
this._proxy = data.proxy || '';
this._key = data.key;
this._secret = data.secret;
this.BASE = `${this._proxy}https://api.binance.com`;
this.orderBook = symbol =>
`${this._proxy}https://dex.binance.org/api/v1/depth?symbol=${symbol}&limit=1000`;
this.trades = symbol =>
`${this._proxy}https://dex.binance.org/api/v1/trades?symbol=${symbol}&limit=20`;
this.streams = {
depth: symbol => `${symbol.toLowerCase()}`,
kline: (symbol, interval) => `${symbol.toLowerCase()}${this.times[interval]}`,
trade: symbol => `${symbol.toLowerCase()}`
};
this.times = {
1: '1m',
3: '3m',
5: '5m',
15: '15m',
30: '30m',
60: '1h',
120: '2h',
240: '4h',
360: '6h',
480: '8h',
720: '12h',
'1D': '1d',
'3D': '3d',
'1W': '1w',
'1M': '1M'
};
this.status = {
NEW: 'open',
PARTIALLY_FILLED: 'open',
FILLED: 'closed',
CANCELED: 'canceled',
REJECTED: 'canceled',
EXPIRED: 'canceled'
};
this.type = {
LIMIT: 'limit',
MARKET: 'market',
STOP_LOSS: 'stop_loss',
STOP_LOSS_LIMIT: 'stop_loss_limit',
TAKE_PROFIT: 'take_profit',
TAKE_PROFIT_LIMIT: 'take_profit_limit',
LIMIT_MAKER: 'limit_maker'
};
this.types = {
limit: 'LIMIT',
market: 'MARKET',
stop_loss: 'STOP_LOSS',
stop_loss_limit: 'STOP_LOSS_LIMIT',
take_profit: 'TAKE_PROFIT',
take_profit_limit: 'TAKE_PROFIT_LIMIT'
};
this.stable_coins = ['USD', 'USDT', 'PAX', 'SDC'];
this.toOrig = {};
this.fromOrig = {};
fetch(`${this._proxy}https://dex.binance.org/api/v1/tokens`).then(r => r.json()).then(rawCoins => {
console.log('rawCoins', rawCoins);
rawCoins.forEach(coin => {
this.toOrig[coin.symbol] = coin.original_symbol;
this.fromOrig[coin.original_symbol] = coin.symbol;
});
console.log(this.fromOrig);
console.log(this.toOrig);
});
}
getOrderTypes() {
return ['limit'];
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit']
},
margin: {
isActive: false
},
intervals: this.getSupportedInterval()
};
}
_setupWebSocket(eventHandler, path, type) {
if (this._sockets[type]) {
this._sockets[type].close();
}
const fullPath = this._mainUrl + path;
this._sockets[type] = new ReWS(fullPath, [], {
WebSocket,
connectionTimeout: 5000,
debug: false
});
this._sockets[type].onmessage = event => {
const res = JSON.parse(event.data);
eventHandler(res);
};
return this._sockets[type];
}
closeTrade() {
if (this._sockets.trade) this._sockets.trade.close();
}
closeOB() {
if (this._sockets.orderbook) this._sockets.orderbook.close();
}
closeKline() {
if (this._sockets.kline) this._sockets.kline.close();
}
onTrade(symbol, eventHandler) {
const splitSymbol = symbol.split(/[:/]/);
const newSymbol = this.fromOrig[splitSymbol[0]] + '_' + this.fromOrig[splitSymbol[1]];
const handler = res => {
const side = res.m ? 'sell' : 'buy';
const trade = {
id: res.f,
side,
timestamp: res.T,
price: +res.p,
amount: +res.q,
symbol: res.s,
exchange: 'binance'
};
eventHandler(trade);
};
fetch(this.trades(newSymbol))
.then(r => r.json())
.then(res => {
res.forEach(raw => {
const side = raw.isBuyerMaker ? 'buy' : 'sell';
const trade = {
id: raw.id,
side,
timestamp: raw.time,
price: +raw.price,
amount: +raw.qty,
symbol,
exchange: 'binance'
};
eventHandler(trade);
});
return this._setupWebSocket(handler, this.streams.trade(newSymbol), 'trade');
});
}
onDepthUpdate(symbol, eventHandler) {
const splitSymbol = symbol.split(/[:/]/);
const newSymbol = this.fromOrig[splitSymbol[0]] + '_' + this.fromOrig[splitSymbol[1]];
const uBuffer = {
asks: [],
bids: [],
type: 'update',
exchange: 'binance',
symbol
};
let SnapshotAccepted = false;
fetch(this.orderBook(newSymbol))
.then(r => r.json())
.then(res => {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'binance',
symbol
};
res.asks.forEach(r => data.asks.push([+r[0], +r[1]]));
res.bids.forEach(r => data.bids.push([+r[0], +r[1]]));
eventHandler(data);
});
const handler = res => {
if (SnapshotAccepted) {
const data = {
asks: [],
bids: [],
type: 'update',
exchange: 'binance',
symbol
};
res.a.forEach(r => data.asks.push([+r[0], +r[1]]));
res.b.forEach(r => data.bids.push([+r[0], +r[1]]));
eventHandler(data);
} else {
res.a.forEach(r => uBuffer.asks.push([+r[0], +r[1]]));
res.b.forEach(r => uBuffer.bids.push([+r[0], +r[1]]));
}
};
const socket = this._setupWebSocket(handler, this.streams.depth(newSymbol), 'orderbook');
socket.onopen = () => {
fetch(this.orderBook(newSymbol))
.then(r => r.json())
.then(res => {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'binance',
symbol
};
res.asks.forEach(r => data.asks.push([+r[0], +r[1]]));
res.bids.forEach(r => data.bids.push([+r[0], +r[1]]));
eventHandler(data);
SnapshotAccepted = true;
eventHandler(uBuffer);
});
};
return socket;
}
onKline(symbol, interval, eventHandler) {
const splitSymbol = symbol.split(/[:/]/);
const newSymbol = this.fromOrig[splitSymbol[0]] + '_' + this.fromOrig[splitSymbol[1]];
const handler = data => {
const newData = {
close: +data.k.c,
high: +data.k.h,
low: +data.k.l,
open: +data.k.o,
time: +data.k.t,
volume: +data.k.v
};
eventHandler(newData);
};
return this._setupWebSocket(handler, this.streams.kline(newSymbol, interval), 'kline');
}
async getPairs() {
return fetch(`${this._proxy}https://dex.binance.org/api/v1/ticker/24hr`)
.then(r => r.json())
.then(r => {
const pairs = {
BTC: [],
ALT: [],
STABLE: []
};
const fullList = {};
r.forEach(pair => {
const [quote, base] = pair.symbol.split('_');
const origBase = this.toOrig[base];
const origQuote = this.toOrig[quote];
const symbol = `${origQuote}/${origBase}`;
const data = {
symbol,
volume: +pair.quoteVolume,
priceChangePercent: +pair.priceChangePercent,
price: +pair.lastPrice,
high: +pair.highPrice,
low: +pair.lowPrice,
quote: origQuote,
base: origBase
};
if (data.price !== 0) {
if (base === 'BTC') {
pairs[base].push(data);
} else if (this.stable_coins.indexOf(base) !== -1) {
pairs.STABLE.push(data);
} else {
pairs.ALT.push(data);
}
fullList[symbol] = data;
}
});
return [pairs, fullList];
});
}
async getKline(pair, interval, start, end) {
const [quote, base] = pair.split('/');
const symbol = `${this.fromOrig[quote]}_${this.fromOrig[base]}`;
return fetch(
`${this._proxy}https://dex.binance.org/api/v1/klines?symbol=${symbol}&interval=${
this.times[interval]
}&endTime=${end * 1000}&limit=1000`
)
.then(r => r.json())
.then(r => {
const newcandle = [];
r.map(obj =>
newcandle.push({
time: obj[0],
open: +obj[1],
high: +obj[2],
low: +obj[3],
close: +obj[4],
volume: +obj[5]
})
);
return newcandle;
});
}
async getBalance(credentials) {
return this._pCall('/v3/account', { recvWindow: RecvWindow }, 'GET', credentials).then(r => {
const newData = { exchange: {} };
if (r && r.balances) {
r.balances.forEach(c => {
if (+c.free !== 0 || +c.locked !== 0) {
newData.exchange[c.asset] = {
coin: c.asset,
free: +c.free,
used: +c.locked,
total: +c.free + +c.locked
};
}
});
}
return newData;
});
}
async getOpenOrders(credentials, { pair } = {}) {
const symbol = pair ? pair.replace('/', '') : '';
const data = { recvWindow: RecvWindow };
if (symbol) {
data.symbol = symbol;
}
return this._pCall('/v3/openOrders', data, 'GET', credentials).then(r => {
const newData = [];
if (r) {
r.forEach(order => {
const responsePair = pair ? symbol : order.symbol;
const base =
responsePair.indexOf('USDT') === -1
? responsePair.substr(responsePair.length - 3)
: responsePair.substr(responsePair.length - 4);
const target =
base === 'USDT'
? responsePair.substr(0, responsePair.length - 4)
: responsePair.substr(0, responsePair.length - 3);
const responseSymbol = `${target}/${base}`;
newData.push({
id: order.orderId,
timestamp: order.time,
lastTradeTimestamp: order.updateTime,
status: this.status[order.status],
symbol: responseSymbol,
type: this.type[order.type],
side: order.side.toLowerCase(),
price: +order.price,
amount: +order.origQty,
executed: +order.executedQty,
filled: (+order.executedQty / +order.origQty) * 100,
remaining: +order.origQty - +order.executedQty,
cost: order.cummulativeQuoteQty > 0 ? +order.cummulativeQuoteQty : 0,
fee: {
symbol: base,
value: order.cummulativeQuoteQty > 0 ? order.cummulativeQuoteQty * 0.001 : 0
}
});
});
}
return newData.reverse();
});
}
async getAllOrders(credentials, { pair, status, orderId } = {}) {
const symbol = pair ? pair.replace('/', '') : '';
const data = { recvWindow: RecvWindow, symbol };
if (!symbol) {
throw Error('Need pass symbol argument');
}
if (orderId) {
data.orderId = orderId;
}
return this._pCall('/v3/allOrders', data, 'GET', credentials)
.then(r => {
const newData = [];
if (r) {
if (orderId) r = r.filter(order => order.orderId === orderId);
r.forEach(order => {
const base = pair.split('/')[1];
const formatedOrder = {
id: order.orderId,
timestamp: order.time,
lastTradeTimestamp: order.updateTime,
status: this.status[order.status],
symbol: pair,
type: this.type[order.type],
side: order.side.toLowerCase(),
price: +order.cummulativeQuoteQty
? +order.cummulativeQuoteQty / +order.executedQty
: +order.price,
amount: +order.origQty,
filled: +order.executedQty,
remaining: +order.origQty - +order.executedQty,
cost: order.cummulativeQuoteQty > 0 ? +order.cummulativeQuoteQty : 0,
fee: {
symbol: base,
value: order.cummulativeQuoteQty > 0 ? order.cummulativeQuoteQty * 0.001 : 0
}
};
if ((status && formatedOrder.status === status) || !status) {
newData.push(formatedOrder);
}
});
}
return newData.reverse();
})
.catch(err => {
throw Error(err);
});
}
async getClosedOrders(credentials, { pair } = {}) {
return this.getAllOrders(credentials, { pair, status: 'closed' });
}
async cancelOrder(credentials, { pair, orderId } = {}) {
const symbol = pair ? pair.replace('/', '') : '';
const data = { recvWindow: RecvWindow, symbol, orderId };
if (!symbol) {
throw Error('Need pass symbol argument');
}
if (!orderId) {
throw Error('Need pass orderId argument');
}
return this._pCall('/v3/order', data, 'DELETE', credentials).then(() =>
this.getAllOrders(credentials, { pair, type: '', orderId })
);
}
// async createOrder(credentials, data) {
// if (!data) {
// throw Error('Need pass oder data object');
// }
// if (!data.type) {
// throw Error('Need pass order type');
// }
// if (!data.pair) {
// throw Error('Need pass order pair');
// }
// if (!data.side) {
// throw Error('Need pass order side');
// }
// if (!data.volume) {
// throw Error('Need pass order volume');
// }
// const symbol = data.pair.replace('/', '');
// return fetch(`${this.BASE}/api/v1/exchangeInfo`)
// .then(r => r.json())
// .then(exchangeInfo => {
// const symbolInfo = exchangeInfo.symbols.find(e => e.symbol === symbol);
// if (!symbolInfo) {
// throw Error('You pass wrong symbol');
// }
// const lotFilter = symbolInfo.filters.find(el => el.filterType === 'LOT_SIZE');
// const step = +lotFilter.stepSize;
//
// const priceFilter = symbolInfo.filters.find(el => el.filterType === 'PRICE_FILTER');
// const precision = +priceFilter.tickSize;
//
// const one = new Decimal(1);
// const step_d = new Decimal(step);
// const precision_d = new Decimal(precision);
// const volume_d = new Decimal(data.volume);
// const price_d = new Decimal(data.price);
//
// const volumePres = volume_d
// .div(step_d)
// .floor()
// .mul(step_d);
// const pricePres = price_d
// .div(precision_d)
// .floor()
// .mul(precision_d);
// // const volumePres = Math.floor(data.volume / step) / (1 / step);
// // const pricePres = Math.floor(data.price / precision) / (1 / precision);
//
// if (data.type === 'market') {
// const payload = {
// recvWindow: RecvWindow,
// symbol,
// type: this.types[data.type],
// side: data.side.toUpperCase(),
// quantity: volumePres.toNumber(),
// };
// return this._pCall('/v3/order', payload, 'POST', credentials).then(r => {
// if (!r.orderId) {
// throw Error(r.msg);
// }
// return this.getAllOrders(credentials, {
// pair: data.pair,
// status: '',
// orderId: r.orderId,
// });
// });
// // .then(r => this.getAllOrders(pair_req, "", order_id))
// }
// if (data.type === 'limit') {
// if (!data.price) {
// throw Error('Need pass order price');
// }
//
// const payload = {
// recvWindow: RecvWindow,
// symbol,
// type: this.types[data.type],
// side: data.side.toUpperCase(),
// quantity: volumePres.toNumber(),
// price: pricePres.toNumber(),
// timeInForce: 'GTC',
// };
// return this._pCall('/v3/order', payload, 'POST', credentials).then(r => {
// if (!r.orderId) {
// throw Error(r.msg);
// }
// return this.getAllOrders(credentials, {
// pair: data.pair,
// status: '',
// orderId: r.orderId,
// });
// });
// }
// // else if (type === 'stop_loss') {
// // if (!stopPrice) {
// // throw Error("Need pass order price")
// // }
// //
// // const data = {
// // recvWindow : RecvWindow,
// // symbol : symbol,
// // type : this.types[type],
// // side : side.toUpperCase(),
// // quantity : volume,
// // stopPrice : stopPrice
// // };
// // return this._pCall('/v3/order', data, "POST")
// // }
// // else if (type === 'stop_loss_limit') {
// // }
// // else if (type === 'take_profit') {
// // }
// // else if (type === 'take_profit_limit') {
// // }
//
// throw Error('Unexpected order type');
// });
// }
}