websocket-crypto-api
Version:
402 lines (360 loc) • 11.6 kB
JavaScript
import Exchanges from './baseExchange';
const axios = require('axios');
const crypto = require('crypto');
class Crex24 extends Exchanges {
constructor(data = {}) {
super();
this._key = data.key;
this._secret = data.secret;
this.name = 'Crex24';
this._proxy = data.proxy || '';
this.URL = `${this._proxy}https://api.crex24.com`;
this.stable_coins = ['RUB', 'USD', 'CNY', 'JPY', 'EUR'];
this.times = {
1: '1m',
3: '3m',
5: '5m',
15: '15m',
30: '30m',
60: '1h',
240: '4h',
'1D': '1d',
'1W': '1w',
'1M': '1mo'
};
this.ms = {
1: 60 * 1000,
3: 3 * 60 * 1000,
5: 5 * 60 * 1000,
15: 15 * 60 * 1000,
30: 30 * 60 * 1000,
60: 60 * 60 * 1000,
240: 4 * 60 * 60 * 1000,
'1D': 24 * 60 * 60 * 1000,
'1W': 7 * 24 * 60 * 1000,
'1M': '1mo'
};
this._sockets = {};
this.status = {
submitting: 'open',
unfilledActive: 'open',
partiallyFilledActive: 'open',
filled: 'closed',
unfilledCancelled: 'canceled',
partiallyFilledCancelled: 'canceled',
waiting: 'open'
};
this.cashCandles = {};
}
//PUBLIC METHODS
getOrderTypes() {
return ['limit', 'market'];
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit']
},
margin: {
isActive: false
},
intervals: this.getSupportedInterval()
};
}
// OB with 1 ask and bid limit
getOrderBook(pair) {
let symbol = pair.replace('/', '-');
return axios(
`${this.URL}/v2/public/orderBook?instrument=${symbol}&limit=10000`
).then(res => {
const result = {
bids: [],
asks: [],
type: 'snapshot',
exchange: 'crex24',
symbol
};
result.bids = res.data.buyLevels.map(el => {
return [+el.price, +el.volume];
});
result.asks = res.data.sellLevels.map(el => {
return [+el.price, +el.volume];
});
return result;
});
}
getTrades(pair) {
const symbol = pair.replace('/', '-');
return axios.get(`${this.URL}/v2/public/recentTrades?instrument=${symbol}`).then(res => {
const trades = res.data.map(trade => {
const d = new Date(trade.timestamp);
return {
id: d.getTime(),
side: trade.side,
timestamp: d.getTime(),
price: +trade.price,
amount: +trade.volume,
symbol: pair,
exchange: 'crex24'
};
});
return trades.reverse();
});
}
onDepthUpdate(symbol, eventHandler) {
this._sockets['orderbook'] ? clearInterval(this._sockets['orderbook']) : 0;
this.getOrderBook(symbol).then(eventHandler);
this._sockets['orderbook'] = setInterval(() => this.getOrderBook(symbol).then(eventHandler), 3000);
}
onTrade(symbol, eventHandler) {
this._sockets['trade'] ? clearInterval(this._sockets['trade']) : 0;
this.getTrades(symbol).then(res => res.forEach(eventHandler));
let lastId = 0;
this._sockets['trade'] = setInterval(() => this.getTrades(symbol).then(res => {
res.filter(trade => trade.id > lastId).forEach(eventHandler);
lastId = res[res.length - 1].id;
}), 3000);
}
onKline(symbol, interval, eventHandler) {
this._sockets['kline'] ? clearInterval(this._sockets['kline']) : 0;
this.getKline(symbol, interval, 0, 0, 1).then(res => res.forEach(eventHandler));
this._sockets['kline'] = setInterval(() => this.getKline(symbol, interval, 0, 0, 1).then(res => eventHandler[res[0]]), 3000);
}
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);
}
async getPairs() {
return axios.get(`${this.URL}/v2/public/tickers`).then(res => {
const pairs = {
BTC: [],
ALT: [],
STABLE: []
};
const fullList = {};
res.data.forEach(pair => {
const [target, base] = pair.instrument.split('-');
const symbol = pair.instrument.replace('-', '/');
const data = {
symbol,
volume: +pair.quoteVolume,
priceChangePercent: +pair.percentChange,
price: +pair.last,
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, limit = 1000) {
if (!end) end = new Date().getTime() / 1000;
const symbol = pair.replace('/', '-');
const msInterval = this.ms[interval];
const cash = this.cashCandles[`${symbol}-${msInterval}`];
if (end !== 0 && !!cash && cash[cash.length - 1].time + msInterval >= end * 1000) {
return cash;
}
const klines = await fetch(
`${this.URL}/v2/public/ohlcv?instrument=${symbol}&granularity=${this.times[interval]}&limit=${limit}`
).then(r => r.json())
.then(r => {
const newcandle = [];
r.map(obj => {
const d = new Date(obj.timestamp);
newcandle.push({
time: d.getTime(),
open: +obj.open,
high: +obj.high,
low: +obj.low,
close: +obj.close,
volume: +obj.volume
});
});
return newcandle;
}).then(rawKline => {
if (interval === '1M') return rawKline;
const kline = [];
let prevTime = -1;
rawKline.forEach(candle => {
if (prevTime !== -1 && candle.time - msInterval !== prevTime) {
while (prevTime !== candle.time) {
const newCandle = {
time: prevTime + msInterval,
open: candle.open,
high: candle.open,
low: candle.open,
close: candle.open,
volume: 0
};
kline.push(newCandle);
prevTime = newCandle.time;
}
}
prevTime = candle.time;
kline.push(candle);
});
return kline;
});
this.cashCandles[`${symbol}-${msInterval}`] = klines;
return klines;
}
//PRIVATE METHODS
privateCall(path, apiKey, apiSecret, method = 'GET', data) {
const nonce = Date.now();
const key = Buffer(apiSecret, 'base64');
const message = method === 'POST' ? path + nonce + JSON.stringify(data) : path + nonce;
const hmac = crypto.createHmac('sha512', key);
const signature = hmac.update(message).digest('base64');
return axios({
url: this.URL + path,
method,
headers: {
'X-CREX24-API-KEY': apiKey || this._key,
'X-CREX24-API-NONCE': nonce,
'X-CREX24-API-SIGN': signature
},
data
})
.then(res => res.data);
}
getBalance({ apiKey, apiSecret }) {
const path = '/v2/account/balance';
return this.privateCall(path, apiKey, apiSecret)
.then(res => {
const result = { exchange: {} };
res.forEach(element => {
result.exchange[element.currency] = {
coin: element.currency,
free: element.available,
used: element.reserved,
total: element.available + element.reserved
};
});
return result;
});
}
getOpenOrders({ apiKey, apiSecret }) {
const path = '/v2/trading/activeOrders';
return this.privateCall(path, apiKey, apiSecret).then(res => {
return res.map(order => {
return {
id: order.id,
timestamp: new Date(order.timestamp).getTime(),
lastTradeTimestamp: new Date(order.lastUpdate).getTime(),
status: this.status[order.status],
symbol: order.instrument.replace('-', '/'),
type: order.type,
side: order.side,
price: +order.price,
stopPx: +order.stopPrice,
amount: +order.volume,
executed: +order.volume - +order.remainingVolume,
filled: ((+order.volume - +order.remainingVolume) / +order.volume) * 100,
remaining: +order.remainingVolume,
cost: order.price * (+order.volume - +order.remainingVolume),
fee: {
symbol: 0,
value: 0
}
};
});
});
}
async getClosedOrders({ apiKey, apiSecret }, { pair } = {}) {
return this.privateCall(`/v2/trading/orderHistory?instrument=${pair.replace('/', '-')}&limit=1000`, apiKey, apiSecret).then(res => {
return res.map(order => {
return {
id: order.id,
timestamp: new Date(order.timestamp).getTime(),
lastTradeTimestamp: new Date(order.lastUpdate).getTime(),
status: this.status[order.status],
symbol: order.instrument.replace('-', '/'),
type: order.type,
side: order.side,
price: +order.price,
amount: +order.volume,
stopPx: +order.stopPrice,
executed: +order.volume - +order.remainingVolume,
filled: ((+order.volume - +order.remainingVolume) / +order.volume) * 100,
remaining: +order.remainingVolume,
cost: order.price * (+order.volume - +order.remainingVolume),
fee: {
symbol: 0,
value: 0
}
};
});
});
}
async getAllOrders(credentials, { pair, status, orderId } = {}) {
const openOrders = await this.getOpenOrders(credentials).then(r => r.filter(order => order.symbol === pair));
const closeOrders = await this.getClosedOrders(credentials, { pair });
const allOrders = [...openOrders, ...closeOrders];
if (status) return allOrders.filter(order => order.status === status);
if (orderId) return allOrders.filter(order => order.id === orderId);
return allOrders;
}
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 path = '/v2/trading/placeOrder';
const body = {
instrument: data.pair.replace('/', '-'),
side: data.side,
volume: data.volume,
type: data.type
};
if (data.type === 'limit') body.price = data.price;
if (data.type === 'stopLimit') body.stopPrice = data.stopPx;
return this.privateCall(path, apiKey, apiSecret, 'POST', body).then(
res => this.getAllOrders({ apiKey, apiSecret }, { pair: data.pair, orderId: res.id })
);
}
cancelOrder(credentials, { pair, orderId }) {
const { apiKey, apiSecret } = credentials;
const path = '/v2/trading/cancelOrdersById';
const body = {
ids: [orderId]
};
return this.privateCall(path, apiKey, apiSecret, 'POST', body).then(() => this.getAllOrders(credentials, {
pair,
orderId
}));
}
}
export default Crex24;