UNPKG

websocket-crypto-api

Version:
519 lines (439 loc) 13.1 kB
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}) }); } }