websocket-crypto-api
Version:
519 lines (439 loc) • 13.1 kB
JavaScript
import Exchanges from 'websocket-crypto-api/exchanges/baseExchange';
import CryptoJS from "crypto-js";
import querystring from 'querystring';
const sign = (message, apiSecret) => {
return CryptoJS.HmacSHA512(message, apiSecret).toString(CryptoJS.enc.hex);
};
export default class Exmo extends Exchanges {
constructor(data = {}) {
super();
this.name = 'Exmo';
this._sockets = {};
this._proxy = data.proxy || '';
this.nonce = Math.floor(new Date().getTime());
// this._key = data.key;
// this._secret = data.secret;
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', 'USDC', 'PAX', 'EUR', 'RUB', 'UAH'];
this.ms = {
'1': 60,
'5': 300,
'15': 60 * 15,
'30': 30 * 60,
'60': 60 * 60,
'120': 120 * 60,
'240': 240 * 60,
'D': 24 * 60 * 60,
'W': 7 * 24 * 60 * 60,
};
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit'],
},
margin: {
isActive: false,
},
intervals: this.getSupportedInterval(),
};
}
getSupportedInterval() {
return ['1', '5', '15', '30', '60', '120', '240', 'D', 'W'];
}
_setupWebSocket(eventHandler, type) {
if (this._sockets[type]) {
clearInterval(this._sockets[type]);
}
this._sockets[type] = setInterval(() => {
eventHandler();
}, 5000);
return this._sockets[type];
}
closeTrade() {
if (this._sockets.trade) clearInterval(this._sockets.trade);
}
closeOB() {
if (this._sockets.orderbook) clearInterval(this._sockets.orderbook);
}
closeKline() {
if (this._sockets.kline) clearInterval(this._sockets.kline);
}
onTrade(symbol = 'BTC/USDT', eventHandler) {
let lastTrade = 0;
const handler = () => {
this.getTrades(symbol).then(r => {
r.forEach(t => {
if (t.id > lastTrade) {
lastTrade = t.id;
eventHandler(t);
}
});
});
};
handler();
return this._setupWebSocket(
handler,
'trade',
);
}
onDepthUpdate(symbol = 'BTC/USDT', eventHandler) {
const handler = () => {
this.getOrderBook(symbol).then(r => {
eventHandler(r);
});
};
handler();
return this._setupWebSocket(
handler,
'orderbook',
);
}
onKline(symbol = 'BTC/USDT', interval = 60, eventHandler) {
const handler = () => {
const now = Date.now() / 1000 | 0;
this.getKline(symbol, interval, now - this.ms[interval] - 100, now).then(data => {
eventHandler(data[0]);
});
};
handler();
return this._setupWebSocket(
handler,
'kline',
);
}
async getPairs() {
return fetch(`${this._proxy}https://api.exmo.me/v1/ticker/`)
.then(r => r.json())
.then(r => {
const pairs = {
BTC: [],
ALT: [],
STABLE: [],
};
const fullList = {};
Object.keys(r).forEach(name => {
const pair = r[name];
const [target, base] = name.split('_');
const symbol = `${target}/${base}`;
const data = {
symbol,
volume: +pair.vol_curr,
priceChangePercent: 0,
price: +pair.last_trade,
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 getTrades(pair) {
return fetch(`${this._proxy}https://api.exmo.me/v1/trades/?pair=${pair.replace('/', '_')}`)
.then(r => r.json())
.then(r => {
const res = [];
r[pair.replace('/', '_')].forEach(raw => {
res.push({
id: raw.trade_id,
side: raw.type,
timestamp: raw.date * 1000,
price: +raw.price,
amount: +raw.amount,
symbol: pair,
exchange: 'exmo',
});
});
return res.reverse();
});
}
async getOrderBook(pair) {
return fetch(`${this._proxy}https://api.exmo.me/v1/order_book/?pair=${pair.replace('/', '_')}`)
.then(r => r.json())
.then(r => {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'exmo',
symbol: pair,
};
r[pair.replace('/', '_')].ask.forEach(raw => {
data.asks.push([+raw[0], +raw[1]]);
});
r[pair.replace('/', '_')].bid.forEach(raw => {
data.bids.push([+raw[0], +raw[1]]);
});
return data;
});
}
async getKline(pair = 'BTC/USDT', interval = 60, start = 0, end) {
if (!end) end = new Date().getTime() / 1000;
const symbol = pair.replace('/', '_');
return fetch(
`${this._proxy}https://chart.exmoney.com/ctrl/chart/history?symbol=${symbol}&resolution=${interval}&from=${start}&to=${end}`,
)
.then(r => r.json())
.then(r => {
return r.candles.map(obj => {
return {
time: obj.t,
open: +obj.o,
high: +obj.h,
low: +obj.l,
close: +obj.c,
volume: +obj.v,
};
});
});
}
_pCall(path, { apiKey, apiSecret }, data = {}) {
if (!apiKey || !apiSecret) {
throw new Error(
"You need to pass an API key and secret to make authenticated calls."
);
}
data.nonce = Date.now();
const queryData = querystring.stringify(data);
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(queryData),
'Key': apiKey,
'Sign': sign(queryData, apiSecret)
};
const json = JSON.stringify(data);
const requestOptions = {
headers,
method: "POST",
body: queryData
};
return fetch(`https://api.exmo.me/v1/${path}`, requestOptions).then(r=>r.json()).then(r=>{
if (r.error){
throw new Error(r.error)
}
return r
})
}
async getBalance(credentials) {
return this._pCall(
'user_info',
credentials,
).then(r => {
const newData = { exchange: {} };
const filtered = { exchange: {} };
if (r && r.balances && r.reserved) {
Object.entries(r.balances).forEach(([coin, free]) => {
newData.exchange[coin] = {
coin,
free,
};
});
Object.entries(r.reserved).forEach(([coin, used]) => {
newData.exchange[coin].used = used;
newData.exchange[coin].total = newData.exchange[coin].free + used
});
}
Object.entries(newData.exchange).filter(([coin, obj]) => {
if (obj.total > 0 || obj.used > 0 || obj.free > 0) {
filtered.exchange[coin] = obj
}
});
return filtered;
});
}
async getOpenOrders(credentials, { pair } = {}) {
const newData = [];
await this._pCall('user_open_orders', credentials).then(r => {
Object.entries(r).forEach(([symbol, orders]) => {
orders.forEach(order => {
const {type, side} = this.getWebcaType(order.type);
newData.push({
id: order.order_id,
timestamp: order.created,
symbol: symbol.replace('_', '/'),
side,
type,
price: +order.price,
amount: +order.quantity,
})
})
})
});
return newData
}
async getClosedOrders(credentials, { pair: rawPair } = {}) {
if (!rawPair) {
throw new Error('Need pass pair arg')
}
const pair = rawPair ? rawPair.replace('/', '_') : '';
const newData = [];
await this._pCall('user_trades', credentials, {pair}).then(r => {
Object.entries(r).forEach(([symbol, orders]) => {
orders.forEach(order => {
const {type, side} = this.getWebcaType(order.type);
newData.push({
id: order.order_id,
timestamp: order.date,
symbol: symbol.replace('_', '/'),
side,
type,
price: +order.price,
amount: +order.quantity,
})
})
})
});
return newData
}
async getCancelledOrders(credentials) {
const newData = [];
await this._pCall('user_cancelled_orders', credentials).then(r => {
r.forEach(order => {
const {type, side} = this.getWebcaType(r.order_type);
newData.push({
id: order.order_id,
timestamp: order.created,
symbol: order.pair.replace('_', '/'),
side,
type,
price: +order.price,
amount: +order.quantity,
})
})
});
return newData
}
async cancelOrder(credentials, { pair, orderId } = {}) {
if (!orderId) {
throw Error('Need pass orderId argument');
}
await this._pCall('order_cancel', credentials, {order_id: orderId});
return [{}]
}
async getAllOrders(credentials, { pair: rawPair, status, orderId }) {
const pair = rawPair ? rawPair.replace('/', '_') : '';
const openOrders = await this.getOpenOrders(credentials);
const filteredOpen = openOrders && openOrders.filter(order => order.id == orderId).map(order => ({...order, status: 'open'}));
if (filteredOpen && filteredOpen.length) {
return filteredOpen
}
const closedOrders = await this.getClosedOrders(credentials, {pair});
const filteredClosed = closedOrders && closedOrders.filter(order => order.id == orderId).map(order => ({...order, status: 'closed'}));
if (filteredClosed && filteredClosed.length) {
return filteredClosed
}
const cancelledOrders = await this.getCancelledOrders(credentials);
const filteredCancelled = cancelledOrders && cancelledOrders.filter(order => order.id == orderId).map(order => ({...order, status: 'cancel'}));
if ( filteredCancelled && filteredCancelled.length) {
return filteredCancelled
}
return [{}]
}
getExmoType(side, type) {
if (type === 'limit' ) {
if (side === 'sell') {
return 'sell'
}
if (side === 'buy') {
return 'buy'
}
}
if (type === 'market' ) {
if (side === 'sell') {
return 'market_sell'
}
if (side === 'buy') {
return 'market_buy'
}
}
if (type === 'volumeMarket') {
if (side === 'sell') {
return 'market_sell_total'
}
if (side === 'buy') {
return 'market_buy_total'
}
}
return 'buy'
}
getOrderTypes() {
return ['limit', 'market'];
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit', 'market'],
},
margin: {
isActive: false,
},
intervals: this.getSupportedInterval(),
};
}
getWebcaType(exmoType) {
switch (exmoType){
case 'buy':
return {type: 'limit', side: 'buy'};
case 'sell':
return {type: 'limit', side: 'sell'};
case 'market_buy':
return {type: 'market', side: 'buy'};
case 'market_sell':
return {type: 'market', side: 'sell'};
case 'market_buy_total':
return {type: 'volumeMarket', side: 'buy'};
case 'market_sell_total':
return {type: 'volumeMarket', side: 'sell'};
default:
return {type: 'limit', side:'buy'}
}
}
async createOrder(credentials, {type, pair: rawPair, side, volume, price=0} = {}) {
const pair = rawPair ? rawPair.replace('/', '_') : '';
return this._pCall('order_create', credentials, {pair, price, quantity: volume, type: this.getExmoType(side, type) }).then((r)=>{
return this.getAllOrders(credentials, {pair, orderId: r.order_id})
});
}
}