websocket-crypto-api
Version:
555 lines (505 loc) • 16.5 kB
JavaScript
import Exchanges from './baseExchange';
import pako from 'pako';
import ReWS from 'reconnecting-websocket';
import CryptoJS from 'crypto-js';
export default class OKEx extends Exchanges {
constructor(proxy = '') {
super();
this.name = 'OKEx';
this._mainUrl = 'wss://real.okex.com:10441/websocket?compress=true';
this._socket = 0;
this._subscriptions = {
trade: '',
orderbook: '',
kline: ''
};
this._pingpongs = {};
this._proxy = proxy;
this._proxy_enable = !!proxy;
this.BASE = `${proxy}https://www.okex.com`;
this.orderBook = symbol => `${
this._proxy_enable ? this._proxy : ''
}https://www.okex.com/api/v1/depth.do?symbol=${symbol.toLowerCase()}&size=200`;
this.streams = {
depth: symbol => `ok_sub_spot_${symbol.toLowerCase()}_depth`,
kline: (symbol, interval) => `ok_sub_spot_${symbol.toLowerCase()}_kline_${this.times[interval]}`,
trade: symbol => `ok_sub_spot_${symbol.toLowerCase()}_deals`,
ticker: symbol => `ok_sub_spot_${symbol.toLowerCase()}_ticker`
};
this.stable_coins = ['USDT'];
this.times = {
1: '1min',
3: '3min',
5: '5min',
15: '15min',
30: '30min',
60: '1hour',
120: '2hour',
240: '4hour',
360: '6hour',
720: '12hour',
'1D': '1day',
'1W': '1week'
};
this.status = {
open: 'open',
ordering: 'open',
part_filled: 'open',
filled: 'closed',
canceling: 'canceled',
cancelled: 'canceled',
failure: 'closed'
};
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,
120: 60 * 60 * 1000,
240: 4 * 60 * 60 * 1000,
360: 6 * 60 * 60 * 1000,
720: 12 * 60 * 60 * 1000,
'1D': 24 * 60 * 60 * 100,
'1W': 7 * 24 * 60 * 60 * 1000
};
this._handlers = {};
this._socketPromise = new Promise(() => {
});
}
getOrderTypes() {
return ['market', 'limit'];
}
getExchangeConfig() {
return {
exchange: {
isActive: true,
componentList: ['open', 'history', 'balance'],
orderTypes: ['limit', 'market']
},
margin: {
isActive: false
},
intervals: this.getSupportedInterval()
};
}
_setupWebSocket(eventHandler, path, type) {
if (!this._socket) {
let Resolver;
this._socketPromise = new Promise((resolve) => {
Resolver = resolve;
});
this._socket = new ReWS(this._mainUrl, [], {
WebSocket:this.websocket,
connectionTimeout: 5000,
debug: false
});
this._socket.binaryType = 'arraybuffer';
this._socket.onopen = () => {
this._pingpongs = setInterval(() => {
try {
ws.send('{"event":"ping"}');
} catch (e) {
clearInterval(this._pingpongs);
}
}, 25000);
Resolver();
};
this._socket.onclose = () => {
clearInterval(this._pingpongs);
};
this._socket.onmessage = ({ data }) => {
if (!(data instanceof String)) {
try {
const res = JSON.parse(pako.inflateRaw(data, { to: 'string' }));
const channel = res[0].channel;
if (this._handlers[channel]) this._handlers[channel](res[0].data);
} catch (err) {
}
}
};
}
this._socketPromise.then(() => {
this._subscriptions[type] = path;
this._handlers[path] = eventHandler;
this._socket.send(`{'event': 'addChannel', 'channel': '${path}'}`);
});
}
closeWebSocket(type) {
if (this._subscriptions[type]) {
const path = this._subscriptions[type];
this._socket.send(`{'event': 'removeChannel', 'channel': '${path}'}`);
this._subscriptions[type] = '';
delete this._handlers[path];
}
}
closeTrade() {
this.closeWebSocket('trade');
}
closeOB() {
this.closeWebSocket('orderbook');
}
closeKline() {
this.closeWebSocket('kline');
}
onTrade(symbol, eventHandler) {
const [quote, base] = symbol.split(/[:/]/);
const newSymbol = `${quote}_${base}`;
const handler = res => {
const side = res[0][4] === 'ask' ? 'sell' : 'buy';
const date = new Date();
const trade = {
id: +res[0][0],
side,
timestamp: date.getTime(),
price: +res[0][1],
amount: +res[0][2],
symbol,
exchange: 'okex'
};
eventHandler(trade);
};
this.getTrades(symbol).then(r => r.forEach(el => eventHandler(el))).then(() => {
return this._setupWebSocket(handler, this.streams.trade(newSymbol), 'trade');
});
}
onDepthUpdate(symbol, eventHandler) {
const [quote, base] = symbol.split(/[:/]/);
const newSymbol = `${quote}_${base}`;
let SnapshotAccepted = false;
const uBuffer = {
type: 'update',
bids: [],
asks: [],
exchange: 'okex',
symbol
};
const handler = res => {
const data = {
asks: [],
bids: [],
type: 'update',
exchange: 'okex',
symbol
};
if (
res.hasOwnProperty('asks')
&& res.asks.length !== 200
&& res.bids.length !== 200
) {
if (SnapshotAccepted) {
res.asks.forEach(r => data.asks.push([+r[0], +r[1]]));
res.bids.forEach(r => data.bids.push([+r[0], +r[1]]));
eventHandler(data);
} else {
res.asks.forEach(r => uBuffer.asks.push([+r[0], +r[1]]));
res.bids.forEach(r => uBuffer.bids.push([+r[0], +r[1]]));
}
}
};
this._setupWebSocket(handler, this.streams.depth(newSymbol), 'orderbook');
fetch(this.orderBook(newSymbol))
.then(r => r.json())
.then(res => {
const data = {
asks: [],
bids: [],
type: 'snapshot',
exchange: 'okex',
symbol
};
res.asks.forEach(r => data.asks.push([+r[0], +r[1]]));
res.bids.forEach(r => data.bids.push([+r[0], +r[1]]));
eventHandler(data);
eventHandler(uBuffer);
SnapshotAccepted = true;
});
}
onKline(symbol, interval, eventHandler) {
const [quote, base] = symbol.split(/[:/]/);
const newSymbol = `${quote}_${base}`;
const handler = data => {
if (!data.hasOwnProperty('result')) {
const newData = {
close: +data[0][4],
high: +data[0][2],
low: +data[0][3],
open: +data[0][1],
time: +data[0][0],
volume: +data[0][5]
};
eventHandler(newData);
}
};
return this._setupWebSocket(
handler,
this.streams.kline(newSymbol, interval),
'kline'
);
}
async getPairs() {
return await fetch(`${this._proxy_enable ? this._proxy : ''}https://www.okex.com/v2/spot/markets/tickers`).then(r => r.json()).then(r => {
const pairs = {
BTC: [],
ALT: [],
STABLE: []
};
const fullList = {};
if (r.data) {
r.data.forEach(pair => {
const base = pair.symbol.split('_')[1].toUpperCase();
const target = pair.symbol.split('_')[0].toUpperCase();
const symbol = `${target}/${base}`;
const data = {
symbol: symbol,
volume: +pair.coinVolume,
priceChangePercent: +pair.changePercentage.split('%')[0],
price: +pair.last,
high: +pair.dayHigh,
low: +pair.dayLow,
quote: target,
base: 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 getTicker(pair) {
const pairs = await this.getPairs().then(r => r[1]);
return pairs[pair];
}
async getKline(pair = 'BTC/USDT', interval = 60, start, end) {
if (!end) end = new Date().getTime() / 1000 | 0;
const symbol = pair.replace('/', '_').toLowerCase();
const startTime = end - this.ms[interval];
return fetch(`${this._proxy_enable ? this._proxy : ''}https://www.okex.com/api/v1/kline.do?symbol=${symbol}&type=${this.times[interval]}&since=${startTime}&size=2000`)
.then(r => r.json()).then(r => {
const newcandle = [];
r.forEach(obj => {
newcandle.push({
time: obj[0],
open: +obj[1],
high: +obj[2],
low: +obj[3],
close: +obj[4],
volume: +obj[5]
});
});
return newcandle;
});
}
async getTrades(pair) {
const symbol = pair.replace('/', '_').toLowerCase();
const list = [];
return fetch(`${this._proxy_enable ? this._proxy : ''}https://www.okex.com/api/v1/trades.do?symbol=${symbol}`).then(r => r.json()).then(r => {
r.forEach(el => {
const date = new Date();
const trade = {
id: el.tid,
side: el.type,
timestamp: el.date_ms,
price: el.price,
amount: el.amount,
symbol:pair,
exchange: 'okex'
};
list.push(trade);
});
return list;
});
}
capitalize(s) {
return s.length ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
makeQueryString(q) {
return q
? `?${Object.keys(q)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(q[k])}`)
.join('&')}`
: '';
}
hmac(request, secret, hash = 'sha256', 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;
}
async privateCall(
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.');
}
return fetch(`${this._proxy}https://www.okex.com/api/general/v3/time`).then(r => r.json()).then(r => r.iso).then((timestamp) => {
const dirUrl = path.replace(/.*\/\/[^\/]*/, '');
const stringData = method === 'POST' ? JSON.stringify(data) : '';
const signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(timestamp + method + dirUrl + stringData, apiSecret));
return fetch(
`${this.BASE}${path}`,
{
method,
headers: {
'OK-ACCESS-KEY': apiKey,
'OK-ACCESS-SIGN': signature,
'OK-ACCESS-TIMESTAMP': timestamp,
'OK-ACCESS-PASSPHRASE': 'oneexbit',
'content-type': 'application/json'
},
json: true,
body: JSON.stringify(data)
}
).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(responce => {
// if (responce.code === -1021) throw new Error('You have different time/date with server. Check your local time/date settings');
// if (responce.code === -1022) throw new Error('Invalid api keys or insufficient permissions');
// if (responce.code === -2010) throw new Error('Account has insufficient balance');
return responce;
});
});
};
getBalance({ apiKey, apiSecret }) {
const path = '/api/spot/v3/accounts';
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.balance - element.available,
total: element.balance
};
});
return result;
});
}
getOpenOrders({ apiKey, apiSecret }) {
const path = '/api/spot/v3/orders_pending';
return this.privateCall(path, apiKey, apiSecret).then(res => {
return res.map(order => {
return {
id: order.order_id,
timestamp: new Date(order.created_at).getTime(),
lastTradeTimestamp: new Date(order.timestamp).getTime(),
status: this.status[order.status],
symbol: order.instrument_id.replace('-', '/'),
type: order.type,
side: order.side,
price: +order.price,
stopPx: +order.stopPrice,
amount: +order.size,
executed: +order.filled_size,
filled: ((+order.size - +order.filled_size) / +order.size) * 100,
remaining: +order.size - +order.filled_size,
cost: order.price * (+order.filled_size),
fee: {
symbol: 0,
value: 0
}
};
});
});
}
async getClosedOrders({ apiKey, apiSecret }, { pair } = {}) {
return this.privateCall(`/api/spot/v3/orders?instrument_id=BTC-USDT&status=filled`, apiKey, apiSecret).then(res => {
return res.map(order => {
return {
id: order.order_id,
timestamp: new Date(order.created_at).getTime(),
lastTradeTimestamp: new Date(order.timestamp).getTime(),
status: this.status[order.status],
symbol: order.instrument_id.replace('-', '/'),
type: order.type,
side: order.side,
price: +order.price,
stopPx: +order.stopPrice,
amount: +order.size,
executed: +order.filled_size,
filled: ((+order.size - +order.filled_size) / +order.size) * 100,
remaining: +order.size - +order.filled_size,
cost: order.price * (+order.filled_size),
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;
}
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 path = '/api/spot/v3/orders';
const body = {
instrument_id: data.pair.replace('/', '-'),
side: data.side,
type: data.type,
margin_trading: '1'
};
if (data.type === 'limit') {
body.price = +data.price;
body.size = +data.volume;
} else if (data.side === 'buy') {
const price = await this.getTicker(data.pair).then(r => r.price);
body.notional = +data.volume * price;
} else {
body.size = +data.volume;
}
return this.privateCall(path, apiKey, apiSecret, 'POST', body).then(
res => this.getAllOrders({ apiKey, apiSecret }, { pair: data.pair, orderId: res.order_id })
);
}
cancelOrder(credentials, { pair, orderId, clientOId }) {
const { apiKey, apiSecret } = credentials;
const path = `/api/spot/v3/cancel_orders/${orderId}`;
const body = {
instrument_id: pair.replace('/', '-'),
order_id: orderId
// 'client-oid': clientOId
};
return this.privateCall(path, apiKey, apiSecret, 'POST', body).then((res) => {
return [{ id: res.order_id }];
});
}
}