websocket-crypto-api
Version:
908 lines (844 loc) • 26.5 kB
JavaScript
/* eslint-disable class-methods-use-this,no-restricted-syntax */
import CryptoJS from 'crypto-js';
import ReWS from 'reconnecting-websocket';
import Exchanges from './baseExchange';
export default class Bitfinex extends Exchanges {
constructor(data = {}) {
super();
this.name = 'Bitfinex';
this._mainUrl = 'wss://api.bitfinex.com/ws/2';
this._sockets = {};
this.tradeLog = [];
this._proxy = data.proxy || '';
this.BASE = `${this._proxy}https://api.bitfinex.com`;
this._key = data.key;
this._secret = data.secret;
this._pCall = this.privateCall();
this._pCallv1 = this.privateCallv1();
this.last_time = Date.now();
this.v1Chain = Promise.resolve(0);
this.v2Chain = Promise.resolve(0);
this.streams = {
depth: symbol =>
JSON.stringify({
event: 'subscribe',
channel: 'book',
symbol,
freq: 'F1'
}),
depthLevel: (symbol, level) =>
`${
this._proxy
}https://api.bitfinex.com/v1/book/${symbol}?limit_bids=${level}&limit_asks=${level}`,
kline: (symbol, interval) =>
JSON.stringify({
event: 'subscribe',
channel: 'candles',
key: `trade:${this.times[interval]}:${symbol}`
}),
trade: symbol =>
JSON.stringify({
event: 'subscribe',
channel: 'trades',
symbol
}),
ticker: symbol =>
JSON.stringify({
event: 'subscribe',
channel: 'ticker',
symbol
})
};
this.times = {
1: '1m',
5: '5m',
15: '15m',
30: '30m',
60: '1h',
180: '3h',
360: '6h',
720: '12h',
'1D': '1D',
'1W': '7D',
'1M': '1M'
};
this.TYPE = {
LIMIT: 'margin limit',
MARKET: 'margin market',
STOP: 'margin stop',
'TRAILING STOP': 'margin ts',
FOK: 'margin fok',
'EXCHANGE MARKET': 'market',
'EXCHANGE LIMIT': 'limit',
'EXCHANGE STOP': 'stop',
'EXCHANGE TRAILING STOP': 'ts',
'EXCHANGE FOK': 'fok'
};
this.stable_coins = ['USD', 'EUR', 'JPY', 'GBP'];
}
getOrderTypes() {
return ['market', 'limit'];
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit', 'market']
},
margin: {
isActive: false
},
intervals: this.getSupportedInterval()
};
}
capitalize(s) {
return s.length ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
STATUS(status) {
const statuses = {
ACTIVE: 'open',
EXECUTED: 'closed',
'PARTIALLY FILLED': 'open',
CANCELED: 'canceled'
};
for (const st in statuses) {
if (status.indexOf(st) !== -1) {
return statuses[st];
}
}
return 'close';
}
hmac(request, secret, hash = 'sha384', digest = 'hex') {
const result = CryptoJS[`Hmac${hash.toUpperCase()}`](request, secret);
if (digest) {
const encoding = digest === 'binary' ? 'Latin1' : this.capitalize(digest);
return result.toString(CryptoJS.enc[this.capitalize(encoding)]);
}
return result;
}
privateCall() {
return (path, data = {}, method = 'GET', { apiKey, apiSecret }) => {
if (!apiKey || !apiSecret) {
throw new Error('You need to pass an API key and secret to make authenticated calls.');
}
let resolve;
const result = this.v2Chain.then(() => {
data.nonce = Date.now().toString();
data.request = path;
data.apikey = apiKey;
const signature = `/api${path.split('?')[0]}${data.nonce}${JSON.stringify(data)}`;
const hsig = this.hmac(signature, apiSecret);
return fetch(`${this.BASE}${path}`, {
method,
headers: {
'bfx-nonce': data.nonce,
'bfx-apikey': apiKey,
'bfx-signature': hsig,
'content-type': 'application/json'
},
body: JSON.stringify(data)
})
.then(r => {
if (r.status === 500)
throw new Error(
'Probably, there is another terminal running on this IP. Currently only one terminal per IP allowed'
);
return r.json();
})
.then(r => {
resolve();
return r;
});
});
this.v2Chain = this.v2Chain.then(() => new Promise(res => (resolve = res)));
return result;
};
}
privateCallv1() {
return (path, data = {}, method = 'GET', { apiKey, apiSecret }) => {
if (!apiKey || !apiSecret) {
throw new Error('You need to pass an API key and secret to make authenticated calls.');
}
let resolve;
const result = this.v1Chain.then(() => {
data.nonce = Date.now().toString();
data.request = path;
data.apikey = apiKey;
const payload = Buffer.from(JSON.stringify(data)).toString('base64');
const signature = this.hmac(payload, apiSecret);
return fetch(`${this.BASE}${path}`, {
method,
headers: {
'X-BFX-APIKEY': apiKey,
'X-BFX-PAYLOAD': payload,
'X-BFX-SIGNATURE': signature,
'content-type': 'application/json'
},
body: JSON.stringify(data)
})
.then(r => {
// if (r.status === 400) throw new Error('Probably, there is another terminal running on this IP. Currently only one terminal per IP allowed');
if (r.status === 401) throw new Error('Invalid api keys or insufficient permissions');
if (r.status === 429)
throw new Error(
'Probably, there is another terminal running on this IP. Currently only one terminal per IP allowed'
);
return r.json();
})
.then(r => {
if (r.error && r.error === 'ERR_RATE_LIMIT')
throw new Error(
'Probably, there is another terminal running on this IP. Currently only one terminal per IP allowed'
);
if (r.message && r.message.includes('Invalid order: minimum size'))
throw new Error('Order has invalid amount');
if (r.message && r.message.includes('Invalid order size'))
throw new Error('Order has invalid amount');
if (r.message && r.message.includes('Invalid order: not enough'))
throw new Error('Account has insufficient balance');
return r;
})
.finally(() => resolve());
});
this.v1Chain = this.v1Chain.then(() => new Promise(res => (resolve = res)));
return result;
};
}
_setupWebSocket(eventHandler, path, func = () => {
}, type) {
if (this._sockets[type]) {
this._sockets[type].close();
}
const ws = new ReWS(this._mainUrl, [], {
WebSocket: this.websocket,
connectionTimeout: 5000,
debug: false
});
ws.onopen = () => {
ws.send(path);
if (func) {
func();
}
};
ws.onmessage = event => {
const res = JSON.parse(event.data);
eventHandler(res);
};
this._sockets[type] = ws;
return ws;
}
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 base = splitSymbol[1] === 'USDT' ? 'USD' : splitSymbol[1];
const newSymbol = `t${splitSymbol[0]}${base}`;
const customEventHandler = trade => {
if (this.tradeLog.indexOf(trade.id) === -1) {
this.tradeLog.push(trade.id);
eventHandler(trade);
}
if (this.tradeLog.length > 40) {
this.tradeLog.shift();
}
};
const handler = res => {
if (res.length === 3) {
const side = res[2][2] < 0 ? 'sell' : 'buy';
const trade = {
id: res[2][0],
side,
timestamp: res[2][1],
price: res[2][3],
amount: res[2][2] < 0 ? -res[2][2] : res[2][2],
symbol,
exchange: 'bitfinex'
};
customEventHandler(trade);
} else if (res.length === 2 && Array.isArray(res[1])) {
res[1].reverse().forEach(data => {
const side = data[2] < 0 ? 'sell' : 'buy';
const trade = {
id: data[0],
side,
timestamp: data[1],
price: data[3],
amount: Math.abs(data[2]),
symbol,
exchange: 'bitfinex'
};
customEventHandler(trade);
});
}
};
return this._setupWebSocket(handler, this.streams.trade(newSymbol), () => {
}, 'trade');
}
onDepthUpdate(symbol, eventHandler) {
const splitSymbol = symbol.split(/[:/]/);
const base = splitSymbol[1] === 'USDT' ? 'USD' : splitSymbol[1];
const newSymbol = `t${splitSymbol[0]}${base}`;
const restSymbol = splitSymbol[0] + base;
const uBuffer = {
asks: [],
bids: [],
type: 'update',
exchange: 'bitfinex',
symbol
};
let SnapshotAccepted = false;
const handler = res => {
if (SnapshotAccepted) {
const data = {
asks: [],
bids: [],
type: 'update',
exchange: 'bitfinex',
symbol
};
if (Array.isArray(res)) {
if (res[1].length === 3) {
if (res[1][1] !== 0) {
res[1][2] > 0
? (data.bids = [[res[1][0], res[1][2]]])
: (data.asks = [[res[1][0], -res[1][2]]]);
} else {
res[1][2] > 0 ? (data.bids = [[res[1][0], 0]]) : (data.asks = [[res[1][0], 0]]);
}
eventHandler(data);
}
}
} else if (Array.isArray(res)) {
if (res[1].length === 3) {
if (res[1][1] !== 0) {
res[1][2] > 0
? uBuffer.bids.push([res[1][0], res[1][2]])
: uBuffer.asks.push([res[1][0], -res[1][2]]);
} else {
res[1][2] > 0 ? uBuffer.bids.push([res[1][0], 0]) : uBuffer.asks.push([res[1][0], 0]);
}
}
}
};
fetch(this.streams.depthLevel(restSymbol, 1000))
.then(r => r.json())
.then(res => {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'bitfinex',
symbol
};
res.asks.forEach(r => data.asks.push([+r.price, +r.amount]));
res.bids.forEach(r => data.bids.push([+r.price, +r.amount]));
eventHandler(data);
});
const func = () => {
fetch(this.streams.depthLevel(restSymbol, 1000))
.then(r => r.json())
.then(res => {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'bitfinex',
symbol
};
res.asks.forEach(r => data.asks.push([+r.price, +r.amount]));
res.bids.forEach(r => data.bids.push([+r.price, +r.amount]));
eventHandler(data);
SnapshotAccepted = true;
eventHandler(uBuffer);
});
};
return this._setupWebSocket(handler, this.streams.depth(newSymbol), func, 'orderbook');
}
onKline(symbol, interval, eventHandler) {
const splitSymbol = symbol.split(/[:/]/);
const base = splitSymbol[1] === 'USDT' ? 'USD' : splitSymbol[1];
const newSymbol = `t${splitSymbol[0]}${base}`;
let lastKline = 0;
const handler = response => {
if (Array.isArray(response[1]) && response[1].length === 6) {
const data = response[1];
const newData = {
close: data[2],
high: data[3],
low: data[4],
open: data[1],
time: data[0],
volume: data[5]
};
if (newData.time >= lastKline) {
lastKline = newData.time;
eventHandler(newData);
}
}
};
return this._setupWebSocket(
handler,
this.streams.kline(newSymbol, interval),
() => {
},
'kline'
);
}
async getPairs() {
return fetch(`${this._proxy}https://api.bitfinex.com/v2/tickers?symbols=ALL`)
.then(r => r.json())
.then(r => {
const pairs = {
BTC: [],
ALT: [],
STABLE: []
};
const fullList = {};
r.forEach(pair => {
if (pair[0][0] === 't') {
const base = pair[0].substr(pair[0].length - 3);
const target = pair[0].substr(1, pair[0].length - 4);
const symbol = `${target}/${base}`;
const data = {
symbol,
volume: pair[8] * pair[7],
priceChangePercent: pair[6] * 100,
price: pair[7],
high: pair[9],
low: pair[10],
quote: target,
base,
maxLeverage: 0,
tickSize: 0
};
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 = 'BTC/USD', interval = 60, start, end) {
if (!end) end = new Date().getTime() / 1000;
const splitSymbol = pair.split(/[:/]/);
const base = splitSymbol[1] === 'USDT' ? 'USD' : splitSymbol[1];
const symbol = `t${splitSymbol[0]}${base}`;
return fetch(
`${this._proxy}https://api.bitfinex.com/v2/candles/trade:${
this.times[interval]
}:${symbol}/hist?limit=1000&end=${end * 1000}`
)
.then(r => r.json())
.then(r => {
const newcandle = [];
r.map(obj =>
newcandle.push({
time: obj[0],
open: +obj[1],
high: +obj[3],
low: +obj[4],
close: +obj[2],
volume: +obj[5]
})
);
return newcandle.reverse();
});
}
async getBalance(credentials) {
return this._pCallv1('/v1/balances', {}, 'POST', credentials).then(r => {
const balance = { exchange: {}, trading: {}, deposit: {} };
r.forEach(coin => {
const symbol = coin.currency.toUpperCase();
if (+coin.amount !== 0) {
if (!Object.hasOwnProperty.call(balance, symbol)) {
balance[coin.type][symbol] = {
coin: coin.currency.toUpperCase(),
free: +coin.available,
used: +coin.amount - +coin.available,
total: +coin.amount
};
}
}
});
return balance;
});
}
async transferBalance(data = {}) {
if (!data.from) {
throw Error('Need pass from-wallet type');
}
if (!data.to) {
throw Error('Need pass to-wallet type');
}
if (!data.coin) {
throw Error('Need pass coin name');
}
if (!data.volume) {
throw Error('Need pass coin volume');
}
const payload = {
amount: data.volume.toString(),
currency: data.coin.toUpperCase(),
walletfrom: data.from,
walletto: data.to
};
return this._pCallv1('/v1/transfer', payload, 'POST').then(r => r[0]);
}
async getPairsMargin() {
return this._pCallv1('/v1/margin_infos', {}, 'POST').then((r) => {
const symbols = r[0].margin_limits;
return this.getPairs().then((r) => {
const full_data = {};
const pairs = {};
symbols.forEach((e) => {
const base = e.on_pair.substr(e.on_pair.length - 3);
const target = e.on_pair.substr(0, e.on_pair.length - 3);
const symbol = `${target}/${base}`;
const info = r[1][symbol];
if (pairs[base]) {
pairs[base].push(info);
} else {
pairs[base] = [];
pairs[base].push(info);
}
full_data[symbol] = info;
});
return [pairs, full_data];
});
});
}
async getOpenOrders(credentials, { orderId } = {}) {
return this._pCall('/v2/auth/r/orders', {}, 'POST', credentials).then(r => {
const orders = [];
r.forEach(order => {
const base = order[3].substr(order[3].length - 3);
const target = order[3].substr(1, order[3].length - 4);
const symbol = `${target}/${base}`;
const item = {
id: order[0],
timestamp: order[4],
lastTradeTimestamp: order[5],
status: this.STATUS(order[13]),
symbol,
type: this.TYPE[order[8]],
side: order[7] > 0 ? 'buy' : 'sell',
price: order[16],
amount: Math.abs(order[7]),
executed: Math.abs(order[7]) - Math.abs(order[6]),
filled: (1 - order[6] / order[7]) * 100,
remaining: Math.abs(order[6]),
cost: order[16] * Math.abs(order[7]),
fee: {
symbol: base,
value: 0
}
};
if (orderId) {
if (orderId === item.id) {
orders.push(item);
}
} else {
orders.push(item);
}
});
return orders;
});
}
async getAllOrders(credentials, { pair, status, orderId } = {}) {
let promis = 0;
if (pair) {
const splitSymbol = pair.split(/[:/]/);
const base = splitSymbol[1] === 'USDT' ? 'USD' : splitSymbol[1];
const symbol = `t${splitSymbol[0]}${base}`;
promis = this._pCall(`/v2/auth/r/orders/${symbol}/hist`, {}, 'POST', credentials);
} else {
promis = this._pCall('/v2/auth/r/orders/hist', {}, 'POST', credentials);
}
return promis.then(r => {
const orders = [];
r.forEach(order => {
const base = order[3].substr(order[3].length - 3);
const target = order[3].substr(1, order[3].length - 4);
const symbol = `${target}/${base}`;
const data = {
id: order[0],
timestamp: order[4],
lastTradeTimestamp: order[5],
status: this.STATUS(order[13]),
symbol,
type: this.TYPE[order[8]],
side: order[7] > 0 ? 'buy' : 'sell',
price: order[16],
amount: Math.abs(order[7]),
executed: Math.abs(order[7]) - Math.abs(order[6]),
filled: (1 - order[6] / order[7]) * 100,
remaining: Math.abs(order[6]),
cost: order[16] * Math.abs(order[7]),
fee: {
symbol: base,
value: 0
}
};
if (orderId) {
if (data.id === orderId) orders.push(data);
} else if (!status || data.status === status) {
orders.push(data);
}
});
return orders;
});
}
async getClosedOrders(credentials, { pair } = {}) {
const payload = {
pair,
status: 'closed'
};
return this.getAllOrders(credentials, payload);
}
async cancelOrder(credentials, { pair, orderId } = {}) {
return this._pCallv1('/v1/order/cancel', { order_id: orderId }, 'POST', credentials).then(r => {
if (!r.id) {
throw Error(r.message);
}
return [
{
id: r.id,
timestamp: +r.timestamp,
lastTradeTimestamp: Date.now(),
status: 'canceled',
symbol: pair,
type: this.TYPE[r.type.toUpperCase()],
side: r.side,
price: +r.price,
amount: +r.original_amount,
executed: +r.original_amount - +r.remaining_amount,
filled: (1 - +r.remaining_amount / +r.original_amount) * 100,
remaining: +r.remaining_amount,
cost: +r.original_amount * +r.price,
fee: {
symbol: pair.split('/')[1],
value: 0
}
}
];
});
}
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('/', '');
if (data.type === 'market') {
const payload = {
symbol,
amount: data.volume.toString(),
price: '1',
exchange: 'bitfinex',
side: data.side,
type: 'exchange market'
};
return this._pCallv1('/v1/order/new', payload, 'POST', credentials).then(r => {
if (!r.id) {
throw Error(r.message);
}
return [
{
id: r.id,
timestamp: +r.timestamp * 1000,
lastTradeTimestamp: Date.now(),
status: 'close',
symbol: data.pair,
type: this.TYPE[r.type.toUpperCase()],
side: r.side,
price: +r.price,
amount: +r.original_amount,
executed: +r.original_amount - +r.remaining_amount,
filled: (1 - +r.remaining_amount / +r.original_amount) * 100,
remaining: +r.remaining_amount,
cost: +r.original_amount * +r.price,
fee: {
symbol: data.pair.split('/')[1],
value: 0
}
}
];
});
}
if (data.type === 'limit') {
if (!data.price) {
throw Error('Need pass order price');
}
const payload = {
symbol,
amount: data.volume.toString(),
price: data.price.toString(),
exchange: 'bitfinex',
side: data.side,
type: 'exchange limit'
};
return this._pCallv1('/v1/order/new', payload, 'POST', credentials).then(r => {
if (!r.id) {
throw Error(r.message);
}
return this.getAllOrders(credentials, { pair: data.pair, orderId: r.id }).then(order => {
if (order.length) {
return order;
}
return this.getOpenOrders(credentials, { pair: data.pair, orderId: r.id }).then(
openOrder => {
if (openOrder.length) {
return openOrder;
}
return [
{
id: r.id,
timestamp: +r.timestamp * 1000,
lastTradeTimestamp: Date.now(),
status: r.is_live ? 'open' : 'close',
symbol: data.pair,
type: this.TYPE[r.type.toUpperCase()],
side: r.side,
price: +r.price,
amount: +r.original_amount,
executed: +r.original_amount - +r.remaining_amount,
filled: (1 - +r.remaining_amount / +r.original_amount) * 100,
remaining: +r.remaining_amount,
cost: +r.original_amount * +r.price,
fee: {
symbol: data.pair.split('/')[1],
value: 0
}
}
];
}
);
});
});
}
if (data.type === 'margin market') {
const payload = {
symbol,
amount: data.volume.toString(),
price: '1',
exchange: 'bitfinex',
side: data.side,
type: 'market'
};
return this._pCallv1('/v1/order/new', payload, 'POST', credentials).then(r => {
if (!r.id) {
throw Error(r.message);
}
return [
{
id: r.id,
timestamp: +r.timestamp,
lastTradeTimestamp: Date.now(),
status: r.is_live ? 'open' : 'close',
symbol: data.pair,
type: this.TYPE[r.type.toUpperCase()],
side: r.side,
price: +r.price,
amount: +r.original_amount,
executed: +r.original_amount - +r.remaining_amount,
filled: (1 - +r.remaining_amount / +r.original_amount) * 100,
remaining: +r.remaining_amount,
cost: +r.original_amount * +r.price,
fee: {
symbol: data.pair.split('/')[1],
value: 0
}
}
];
});
}
if (data.type === 'margin limit') {
if (!data.price) {
throw Error('Need pass order price');
}
const payload = {
symbol,
amount: data.volume.toString(),
price: data.price.toString(),
exchange: 'bitfinex',
side: data.side,
type: 'limit'
};
return this._pCallv1('/v1/order/new', payload, 'POST', credentials).then(r => {
if (!r.id) {
throw Error(r.message);
}
return [
{
id: r.id,
timestamp: +r.timestamp,
lastTradeTimestamp: Date.now(),
status: r.is_live ? 'open' : 'close',
symbol: data.pair,
type: this.TYPE[r.type.toUpperCase()],
side: r.side,
price: +r.price,
amount: +r.original_amount,
executed: +r.original_amount - +r.remaining_amount,
filled: (1 - +r.remaining_amount / +r.original_amount) * 100,
remaining: +r.remaining_amount,
cost: +r.original_amount * +r.price,
fee: {
symbol: data.pair.split('/')[1],
value: 0
}
}
];
});
}
// 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');
}
}