websocket-crypto-api
Version:
531 lines (466 loc) • 16.9 kB
JavaScript
import ReWS from 'reconnecting-websocket';
import Exchanges from './baseExchange';
import pako from 'pako';
import CryptoJS from 'crypto-js';
export default class HuobiG extends Exchanges {
constructor(proxy) {
super();
this.name = 'Huobi Global';
this._mainUrl = 'wss://api.huobi.pro/ws`';
this._sockets = {};
this._proxy = proxy;
this._proxy_enable = !!proxy;
this.BASE = `${this._proxy_enable ? this._proxy : ''}https://api.huobi.pro`;
this.streams = {
depth: symbol =>
JSON.stringify({ 'sub': `market.${symbol}.depth.step0`, 'id': '133' }),
trade: symbol =>
JSON.stringify({ 'sub': `market.${symbol}.trade.detail`, 'id': '133' }),
kline: (symbol, interval) =>
JSON.stringify({ 'sub': `market.${symbol}.kline.${this.times[interval]}`, 'id': '123' }),
};
this.times = {
'1': '1min',
'5': '5min',
'15': '15min',
'30': '30min',
'60': '60min',
'1D': '1day',
'1W': '1week',
};
this.ms = {
'1': 60 * 1000,
'5': 5 * 60 * 1000,
'15': 15 * 60 * 1000,
'30': 30 * 60 * 1000,
'60': 60 * 60 * 1000,
'1D': 24 * 60 * 60 * 1000,
'1W': 7 * 24 * 60 * 60 * 1000,
};
this.stable_coins = ['USDT', 'HUSD'];
this.status = {
submitted: 'open',
'partial-filled': 'open',
filled: 'closed',
'partial-canceled': 'canceled',
canceled: 'canceled',
};
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit', 'market'],
},
margin: {
isActive: false,
},
intervals: this.getSupportedInterval(),
};
}
_setupWebSocket(eventHandler, path, 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);
ws.onmessage = (e) => {
let reader = new FileReader();
reader.onload = () => {
const res = JSON.parse(pako.inflate(reader.result, { to: 'string' }));
eventHandler(res);
};
reader.readAsBinaryString(e.data);
};
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 newSymbol = symbol.replace('/', '').toLowerCase();
const handler = res => {
if (res.hasOwnProperty('tick')) {
res = res.tick.data;
res.forEach(e => {
const trade = {
id: e.id,
side: e.direction,
timestamp: e.ts,
price: +e.price,
amount: +e.amount,
symbol,
exchange: 'huobi-global',
};
eventHandler(trade);
});
}
};
return this._setupWebSocket(handler, this.streams.trade(newSymbol), 'trade');
}
onDepthUpdate(symbol, eventHandler) {
const newSymbol = symbol.replace('/', '').toLowerCase();
const handler = res => {
if (res.hasOwnProperty('tick')) {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'huobi-global',
symbol,
};
res = res.tick;
res.asks.forEach(r => data.asks.push([+r[0], +r[1]]));
res.bids.forEach(r => data.bids.push([+r[0], +r[1]]));
eventHandler(data);
}
};
this._setupWebSocket(handler, this.streams.depth(newSymbol), 'orderbook');
}
onKline(symbol, interval, eventHandler) {
const newSymbol = symbol.replace('/', '').toLowerCase();
const handler = data => {
if (data.hasOwnProperty('tick')) {
data = data.tick;
const newData = {
close: +data.close,
high: +data.high,
low: +data.low,
open: +data.open,
time: data.id * 1000,
volume: +data.amount,
};
eventHandler(newData);
}
};
return this._setupWebSocket(handler, this.streams.kline(newSymbol, interval), 'kline');
}
async getPairs() {
return await fetch(
`${this._proxy_enable ? this._proxy : 'https://api.huobi.pro/market/tickers'}`,
)
.then(r => r.json())
.then(r => {
const pairs = {
BTC: [],
ALT: [],
STABLE: [],
};
const fullList = {};
r.data.forEach(pair => {
let numSub = 3;
if (pair.symbol.endsWith('ht')) numSub = 2;
if (pair.symbol.endsWith('usdt') || pair.symbol.endsWith('husd')) numSub = 4;
pair.symbol = pair.symbol.toUpperCase();
const base = pair.symbol.substr(pair.symbol.length - numSub);
const target = pair.symbol.substr(0, pair.symbol.length - numSub);
const symbol = `${target}/${base}`;
const data = {
symbol,
volume: pair.vol,
priceChangePercent: 100 - (pair.close / pair.open) * 100,
price: pair.close,
high: pair.high,
low: pair.low,
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) {
const symbol = pair.replace('/', '').toLowerCase();
return fetch(
`${
this._proxy_enable ? this._proxy : ''
}https://api.huobi.pro/market/history/kline?period=${this.times[interval]}&size=2000&symbol=${symbol}`,
)
.then(r => r.json())
.then(r => {
const newCandles = [];
r.data.forEach(obj => {
newCandles.push({
time: obj.id * 1000,
open: +obj.open,
high: +obj.high,
low: +obj.low,
close: +obj.close,
volume: +obj.vol,
});
});
return newCandles.reverse();
});
}
makeQueryString(q) {
return q
? `?${Object.keys(q).sort()
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(q[k])}`)
.join('&')}`
: '';
}
digitFormat(a) {
if (a < 10) {
return `0${a}`;
}
return `${a}`;
}
timeFormat(a) {
const year = this.digitFormat(a.getFullYear());
const month = this.digitFormat(a.getMonth() + 1);
const day = this.digitFormat(a.getDate());
const hour = this.digitFormat(a.getHours());
const minute = this.digitFormat(a.getMinutes());
const second = this.digitFormat(a.getSeconds());
const str = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
return str;
}
async pCall(path, apiKey, apiSecret, method = 'GET', data = {}) {
if (!apiKey || !apiSecret) {
throw new Error('You need to pass an API key and secret to make authenticated calls.');
}
const date = new Date();
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
data.Timestamp = this.timeFormat(date);
data.AccessKeyId = apiKey;
data.SignatureMethod = 'HmacSHA256';
data.SignatureVersion = '2';
const dirUrl = path.replace(/.*\/\/[^\/]*/, '');
// Add new format querry string
// const querryString = `AccessKeyId=e2xxxxxx-99xxxxxx-84xxxxxx-7xxxx&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2017-05-11T15%3A19%3A30`;
const signString = `${method}\n${this.BASE.replace('https://', '')}\n${path}\n${this.makeQueryString(data).replace('?', '')}`;
const signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(signString, apiSecret));
const dataString = this.makeQueryString(data) + '&Signature=' + encodeURIComponent(signature);
const prom = method === 'GET' ?
fetch(`${this.BASE}${path}${dataString}`, {
method,
headers: { 'Content-Type': 'application/json' },
})
:
fetch(`${this.BASE}${path}${dataString}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return prom.then((r) => {
// if (r.status === 401) throw new Error('Invalid api keys or insufficient permissions');
// if (r.status === 419) throw new Error('Probably, there is another terminal running on this IP. Currently only one terminal per IP allowed');
// 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(response => {
if (response.status === 'error') throw new Error(`huobi ${response['err-msg']}`);
// if (response.code === -1021) throw new Error('You have different time/date with server. Check your local time/date settings');
// if (response.code === -1022) throw new Error('Invalid api keys or insufficient permissions');
// if (response.code === -2010) throw new Error('Account has insufficient balance');
return response;
});
}
getBalance({ apiKey, apiSecret }) {
return this.pCall('/v1/account/accounts', apiKey, apiSecret).then(res => {
const result = { exchange: {} };
const proms = res.data.map(({ id }) => {
return this.pCall(`/v1/account/accounts/${id}/balance`, apiKey, apiSecret).then(res => {
res.data.list.forEach(el => {
if (+el.balance > 0) {
if (!result.exchange[el.currency.toUpperCase()]) {
result.exchange[el.currency.toUpperCase()] = {
coin: el.currency.toUpperCase(),
free: 0,
used: 0,
total: 0,
};
}
if (el.type === 'trade') result.exchange[el.currency.toUpperCase()].free = +el.balance;
if (el.type === 'frozen') result.exchange[el.currency.toUpperCase()].used = +el.balance;
result.exchange[el.currency.toUpperCase()].total = result.exchange[el.currency.toUpperCase()].free + result.exchange[el.currency.toUpperCase()].used;
}
});
});
});
return Promise.all(proms).then(() => {
console.log('getBalance', result);
return result;
});
});
}
async getOpenOrders({ apiKey, apiSecret }) {
return this.pCall('/v1/order/openOrders', apiKey, apiSecret, 'GET', {
size: 2000,
}).then(res => {
return res.data.map(order => {
const [side, type] = order.type.split('-');
let numSub = 3;
if (order.symbol.endsWith('ht')) numSub = 2;
if (order.symbol.endsWith('usdt') || order.symbol.endsWith('husd')) numSub = 4;
order.symbol = order.symbol.toUpperCase();
const base = order.symbol.substr(order.symbol.length - numSub);
const target = order.symbol.substr(0, order.symbol.length - numSub);
const symbol = `${target}/${base}`;
return {
id: order.id,
timestamp: new Date(order['created-at']).getTime(),
lastTradeTimestamp: new Date(order['created-at']).getTime(),
status: this.status[order.state],
symbol: symbol,
type: type,
side: side,
price: +order.price,
stopPx: 0,
amount: +order.amount,
executed: +order['filled-amount'],
filled: ((+order['filled-amount']) / +order.amount) * 100,
remaining: +order.quantity - +order.cumQuantity,
cost: +order.price * (+order.amount),
fee: {
symbol: 0,
value: 0,
},
};
});
}).then(res => {
console.log('getOpenOrders', res);
return res;
});
// account-id: 6683454
// amount: "50.000000000000000000"
// created-at: 1561456814794
// filled-amount: "0.0"
// filled-cash-amount: "0.0"
// filled-fees: "0.0"
// id: 38268159150
// price: "0.090000000000000000"
// source: "web"
// state: "submitted"
// symbol: "adausdt"
// type: "buy-limit"
}
async getAllOrders({ apiKey, apiSecret }, { pair, status, orderId } = {}) {
const closed = await this.getClosedOrders({ apiKey, apiSecret }, { pair });
const opened = await this.getOpenOrders({ apiKey, apiSecret });
console.log('GAVNOVHUIAH', closed, opened);
const allOrders = [...closed, ...opened];
if (orderId) return allOrders.filter(order => order.id === orderId);
if (orderId) return allOrders.filter(order => order.status === status);
return allOrders;
}
async getClosedOrders({ apiKey, apiSecret }, { pair } = {}) {
return this.pCall('/v1/account/accounts', apiKey, apiSecret).then(res => {
const result = [];
const proms = res.data.map(({ id }) => {
return this.pCall('/v1/order/orders', apiKey, apiSecret, 'GET', {
symbol: pair.replace('/', '').toLowerCase(),
states: 'partial-canceled,filled,canceled',
size: 100,
'account-id': id,
}).then(res => {
res.data.forEach(order => {
const [side, type] = order.type.split('-');
let numSub = 3;
if (order.symbol.endsWith('ht')) numSub = 2;
if (order.symbol.endsWith('usdt') || order.symbol.endsWith('husd')) numSub = 4;
order.symbol = order.symbol.toUpperCase();
const base = order.symbol.substr(order.symbol.length - numSub);
const target = order.symbol.substr(0, order.symbol.length - numSub);
const symbol = `${target}/${base}`;
result.push({
id: order.id,
timestamp: new Date(order['created-at']).getTime(),
lastTradeTimestamp: new Date(order['created-at']).getTime(),
status: this.status[order.state],
symbol: symbol,
type: type,
side: side,
price: +order.price,
stopPx: 0,
amount: +order.amount,
executed: +order['filled-amount'],
filled: ((+order['filled-amount']) / +order.amount) * 100,
remaining: +order.quantity - +order.cumQuantity,
cost: +order.price * (+order.amount),
fee: {
symbol: 0,
value: 0,
},
});
});
});
});
return Promise.all(proms).then(() => result);
});
}
async cancelOrder({ apiKey, apiSecret }, { pair, orderId }) {
return this.pCall(`/v1/order/orders/${orderId}/submitcancel`, apiKey, apiSecret, 'POST', {}).then(res => {
console.log('cancelOrder', res);
return this.getAllOrders({ apiKey, apiSecret }, { pair, orderId });
});
}
async createOrder({ apiKey, apiSecret }, 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 info = await this.getPairInfo(data.pair);
return this.pCall('/v1/account/accounts', apiKey, apiSecret).then(res => {
const accId = res.data[0].id;
const payload = {
'account-id': `${accId}`,
'amount': data.volume.toFixed(info['amount-precision']),
'source': 'api',
'symbol': data.pair.replace('/', '').toLowerCase(),
'type': `${data.side}-${data.type}`.toLowerCase(),
};
if (data.type === 'limit') {
if (!data.volume) {
throw Error('Need pass order price');
}
payload.price = (+data.price).toFixed(info['price-precision']);
}
return this.pCall('/v1/order/orders/place', apiKey, apiSecret, 'POST', payload).then(res => {
return this.getAllOrders({ apiKey, apiSecret }, { pair: data.pair, orderId: res.data });
});
});
}
async getPairInfo(pair) {
const [base, quote] = pair.toLowerCase().split('/');
return fetch(this.BASE + '/v1/common/symbols').then(r => r.json()).then(r => {
const res = r.data.filter(coin => coin['base-currency'] === base && coin['quote-currency'] === quote);
// console.log('getPairInfo', r, res);
return res[0];
});
}
}