sfccxt
Version:
A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges
608 lines (585 loc) • 23.2 kB
JavaScript
'use strict';
// ---------------------------------------------------------------------------
const hollaexRest = require ('../hollaex.js');
const { AuthenticationError, BadSymbol, BadRequest } = require ('../base/errors');
const { ArrayCache, ArrayCacheBySymbolById } = require ('./base/Cache');
// ---------------------------------------------------------------------------
module.exports = class hollaex extends hollaexRest {
describe () {
return this.deepExtend (super.describe (), {
'has': {
'ws': true,
'watchBalance': true,
'watchMyTrades': false,
'watchOHLCV': false,
'watchOrderBook': true,
'watchOrders': true,
'watchTicker': false,
'watchTickers': false, // for now
'watchTrades': true,
},
'urls': {
'api': {
'ws': 'wss://api.hollaex.com/stream',
},
'test': {
'ws': 'wss://api.sandbox.hollaex.com/stream',
},
},
'options': {
'watchBalance': {
// 'api-expires': undefined,
},
'watchOrders': {
// 'api-expires': undefined,
},
},
'streaming': {
'ping': this.ping,
},
'exceptions': {
'ws': {
'exact': {
'Bearer or HMAC authentication required': BadSymbol, // { error: 'Bearer or HMAC authentication required' }
'Error: wrong input': BadRequest, // { error: 'Error: wrong input' }
},
},
},
});
}
async watchOrderBook (symbol, limit = undefined, params = {}) {
/**
* @method
* @name hollaex#watchOrderBook
* @description watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
* @param {string} symbol unified symbol of the market to fetch the order book for
* @param {int|undefined} limit the maximum amount of order book entries to return
* @param {object} params extra parameters specific to the hollaex api endpoint
* @returns {object} A dictionary of [order book structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-book-structure} indexed by market symbols
*/
await this.loadMarkets ();
const market = this.market (symbol);
const messageHash = 'orderbook' + ':' + market['id'];
const orderbook = await this.watchPublic (messageHash, params);
return orderbook.limit ();
}
handleOrderBook (client, message) {
//
// {
// "topic":"orderbook",
// "action":"partial",
// "symbol":"ltc-usdt",
// "data":{
// "bids":[
// [104.29, 5.2264],
// [103.86,1.3629],
// [101.82,0.5942]
// ],
// "asks":[
// [104.81,9.5531],
// [105.54,0.6416],
// [106.18,1.4141],
// ],
// "timestamp":"2022-04-12T08:17:05.932Z"
// },
// "time":1649751425
// }
//
const marketId = this.safeString (message, 'symbol');
const channel = this.safeString (message, 'topic');
const market = this.safeMarket (marketId);
const symbol = market['symbol'];
const data = this.safeValue (message, 'data');
let timestamp = this.safeString (data, 'timestamp');
timestamp = this.parse8601 (timestamp);
const snapshot = this.parseOrderBook (data, symbol, timestamp);
let orderbook = undefined;
if (!(symbol in this.orderbooks)) {
orderbook = this.orderBook (snapshot);
this.orderbooks[symbol] = orderbook;
} else {
orderbook = this.orderbooks[symbol];
orderbook.reset (snapshot);
}
const messageHash = channel + ':' + marketId;
client.resolve (orderbook, messageHash);
}
async watchTrades (symbol, since = undefined, limit = undefined, params = {}) {
/**
* @method
* @name hollaex#watchTrades
* @description get the list of most recent trades for a particular symbol
* @param {string} symbol unified symbol of the market to fetch trades for
* @param {int|undefined} since timestamp in ms of the earliest trade to fetch
* @param {int|undefined} limit the maximum amount of trades to fetch
* @param {object} params extra parameters specific to the hollaex api endpoint
* @returns {[object]} a list of [trade structures]{@link https://docs.ccxt.com/en/latest/manual.html?#public-trades}
*/
await this.loadMarkets ();
const market = this.market (symbol);
symbol = market['symbol'];
const messageHash = 'trade' + ':' + market['id'];
const trades = await this.watchPublic (messageHash, params);
if (this.newUpdates) {
limit = trades.getLimit (symbol, limit);
}
return this.filterBySinceLimit (trades, since, limit, 'timestamp', true);
}
handleTrades (client, message) {
//
// {
// topic: 'trade',
// action: 'partial',
// symbol: 'btc-usdt',
// data: [
// {
// size: 0.05145,
// price: 41977.9,
// side: 'buy',
// timestamp: '2022-04-11T09:40:10.881Z'
// },
// ]
// }
//
const channel = this.safeString (message, 'topic');
const marketId = this.safeString (message, 'symbol');
const market = this.safeMarket (marketId);
const symbol = market['symbol'];
let stored = this.safeValue (this.trades, symbol);
if (stored === undefined) {
const limit = this.safeInteger (this.options, 'tradesLimit', 1000);
stored = new ArrayCache (limit);
this.trades[symbol] = stored;
}
const data = this.safeValue (message, 'data', []);
const parsedTrades = this.parseTrades (data, market);
for (let j = 0; j < parsedTrades.length; j++) {
stored.append (parsedTrades[j]);
}
const messageHash = channel + ':' + marketId;
client.resolve (stored, messageHash);
client.resolve (stored, channel);
}
async watchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
/**
* @method
* @name hollaex#watchMyTrades
* @description watches information on multiple trades made by the user
* @param {string} symbol unified market symbol of the market orders were made in
* @param {int|undefined} since the earliest time in ms to fetch orders for
* @param {int|undefined} limit the maximum number of orde structures to retrieve
* @param {object} params extra parameters specific to the hollaex api endpoint
* @returns {[object]} a list of [order structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-structure
*/
await this.loadMarkets ();
let messageHash = 'usertrade';
let market = undefined;
if (symbol !== undefined) {
market = this.market (symbol);
symbol = market['symbol'];
messageHash += ':' + market['id'];
}
const trades = await this.watchPrivate (messageHash, params);
if (this.newUpdates) {
limit = trades.getLimit (symbol, limit);
}
return this.filterBySymbolSinceLimit (trades, symbol, since, limit, true);
}
handleMyTrades (client, message, subscription = undefined) {
//
// {
// "topic":"usertrade",
// "action":"insert",
// "user_id":"103",
// "symbol":"xht-usdt",
// "data":[
// {
// "size":1,
// "side":"buy",
// "price":0.24,
// "symbol":"xht-usdt",
// "timestamp":"2022-05-13T09:30:15.014Z",
// "order_id":"6065a66e-e9a4-44a3-9726-4f8fa54b6bb6",
// "fee":0.001,
// "fee_coin":"xht",
// "is_same":true
// }
// ],
// "time":1652434215
// }
//
const channel = this.safeString (message, 'topic');
const rawTrades = this.safeValue (message, 'data');
// usually the first message is an empty array
// when the user does not have any trades yet
const dataLength = rawTrades.length;
if (dataLength === 0) {
return 0;
}
if (this.myTrades === undefined) {
const limit = this.safeInteger (this.options, 'tradesLimit', 1000);
this.myTrades = new ArrayCache (limit);
}
const stored = this.myTrades;
const marketIds = {};
for (let i = 0; i < rawTrades.length; i++) {
const trade = rawTrades[i];
const parsed = this.parseTrade (trade);
stored.append (parsed);
const symbol = trade['symbol'];
const market = this.market (symbol);
const marketId = market['id'];
marketIds[marketId] = true;
}
// non-symbol specific
client.resolve (this.myTrades, channel);
const keys = Object.keys (marketIds);
for (let i = 0; i < keys.length; i++) {
const marketId = keys[i];
const messageHash = channel + ':' + marketId;
client.resolve (this.myTrades, messageHash);
}
}
async watchOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
/**
* @method
* @name hollaex#watchOrders
* @description watches information on multiple orders made by the user
* @param {string|undefined} symbol unified market symbol of the market orders were made in
* @param {int|undefined} since the earliest time in ms to fetch orders for
* @param {int|undefined} limit the maximum number of orde structures to retrieve
* @param {object} params extra parameters specific to the hollaex api endpoint
* @returns {[object]} a list of [order structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-structure}
*/
await this.loadMarkets ();
let messageHash = 'order';
let market = undefined;
if (symbol !== undefined) {
market = this.market (symbol);
symbol = market['symbol'];
messageHash += ':' + market['id'];
}
const orders = await this.watchPrivate (messageHash, params);
if (this.newUpdates) {
limit = orders.getLimit (symbol, limit);
}
return this.filterBySymbolSinceLimit (orders, symbol, since, limit, true);
}
handleOrder (client, message, subscription = undefined) {
//
// {
// topic: 'order',
// action: 'insert',
// user_id: 155328,
// symbol: 'ltc-usdt',
// data: {
// symbol: 'ltc-usdt',
// side: 'buy',
// size: 0.05,
// type: 'market',
// price: 0,
// fee_structure: { maker: 0.1, taker: 0.1 },
// fee_coin: 'ltc',
// id: 'ce38fd48-b336-400b-812b-60c636454231',
// created_by: 155328,
// filled: 0.05,
// method: 'market',
// created_at: '2022-04-11T14:09:00.760Z',
// updated_at: '2022-04-11T14:09:00.760Z',
// status: 'filled'
// },
// time: 1649686140
// }
//
// {
// "topic":"order",
// "action":"partial",
// "user_id":155328,
// "data":[
// {
// "created_at":"2022-05-13T08:19:07.694Z",
// "fee":0,
// "meta":{
//
// },
// "symbol":"ltc-usdt",
// "side":"buy",
// "size":0.1,
// "type":"limit",
// "price":55,
// "fee_structure":{
// "maker":0.1,
// "taker":0.1
// },
// "fee_coin":"ltc",
// "id":"d5e77182-ad4c-4ac9-8ce4-a97f9b43e33c",
// "created_by":155328,
// "filled":0,
// "status":"new",
// "updated_at":"2022-05-13T08:19:07.694Z",
// "stop":null
// }
// ],
// "time":1652430035
// }
//
const channel = this.safeString (message, 'topic');
const data = this.safeValue (message, 'data', {});
// usually the first message is an empty array
const dataLength = data.length;
if (dataLength === 0) {
return 0;
}
if (this.orders === undefined) {
const limit = this.safeInteger (this.options, 'ordersLimit', 1000);
this.orders = new ArrayCacheBySymbolById (limit);
}
const stored = this.orders;
let rawOrders = undefined;
if (!Array.isArray (data)) {
rawOrders = [ data ];
} else {
rawOrders = data;
}
const marketIds = {};
for (let i = 0; i < rawOrders.length; i++) {
const order = rawOrders[i];
const parsed = this.parseOrder (order);
stored.append (parsed);
const symbol = order['symbol'];
const market = this.market (symbol);
const marketId = market['id'];
marketIds[marketId] = true;
}
// non-symbol specific
client.resolve (this.orders, channel);
const keys = Object.keys (marketIds);
for (let i = 0; i < keys.length; i++) {
const marketId = keys[i];
const messageHash = channel + ':' + marketId;
client.resolve (this.orders, messageHash);
}
}
async watchBalance (params = {}) {
/**
* @method
* @name hollaex#watchBalance
* @description query for balance and get the amount of funds available for trading or funds locked in orders
* @param {object} params extra parameters specific to the hollaex api endpoint
* @returns {object} a [balance structure]{@link https://docs.ccxt.com/en/latest/manual.html?#balance-structure}
*/
const messageHash = 'wallet';
return await this.watchPrivate (messageHash, params);
}
handleBalance (client, message) {
//
// {
// topic: 'wallet',
// action: 'partial',
// user_id: 155328,
// data: {
// eth_balance: 0,
// eth_available: 0,
// usdt_balance: 18.94344188,
// usdt_available: 18.94344188,
// ltc_balance: 0.00005,
// ltc_available: 0.00005,
// },
// time: 1649687396
// }
//
const messageHash = this.safeString (message, 'topic');
const data = this.safeValue (message, 'data');
const keys = Object.keys (data);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const parts = key.split ('_');
const currencyId = this.safeString (parts, 0);
const code = this.safeCurrencyCode (currencyId);
const account = (code in this.balance) ? this.balance[code] : this.account ();
const second = this.safeString (parts, 1);
const freeOrTotal = (second === 'available') ? 'free' : 'total';
account[freeOrTotal] = this.safeString (data, key);
this.balance[code] = account;
}
this.balance = this.safeBalance (this.balance);
client.resolve (this.balance, messageHash);
}
async watchPublic (messageHash, params = {}) {
const url = this.urls['api']['ws'];
const request = {
'op': 'subscribe',
'args': [ messageHash ],
};
const message = this.extend (request, params);
return await this.watch (url, messageHash, message, messageHash);
}
async watchPrivate (messageHash, params = {}) {
this.checkRequiredCredentials ();
let expires = this.safeString (this.options, 'ws-expires');
if (expires === undefined) {
const timeout = parseInt (this.timeout / 1000);
expires = this.sum (this.seconds (), timeout);
expires = expires.toString ();
// we need to memoize these values to avoid generating a new url on each method execution
// that would trigger a new connection on each received message
this.options['ws-expires'] = expires;
}
const url = this.urls['api']['ws'];
const auth = 'CONNECT' + '/stream' + expires;
const signature = this.hmac (this.encode (auth), this.encode (this.secret));
const authParams = {
'api-key': this.apiKey,
'api-signature': signature,
'api-expires': expires,
};
const signedUrl = url + '?' + this.urlencode (authParams);
const request = {
'op': 'subscribe',
'args': [ messageHash ],
};
const message = this.extend (request, params);
return await this.watch (signedUrl, messageHash, message, messageHash);
}
handleErrorMessage (client, message) {
//
// { error: 'Bearer or HMAC authentication required' }
// { error: 'Error: wrong input' }
//
const error = this.safeInteger (message, 'error');
try {
if (error !== undefined) {
const feedback = this.id + ' ' + this.json (message);
this.throwExactlyMatchedException (this.exceptions['ws']['exact'], error, feedback);
}
} catch (e) {
if (e instanceof AuthenticationError) {
return false;
}
}
return message;
}
handleMessage (client, message) {
//
// pong
//
// { message: 'pong' }
//
// trade
//
// {
// topic: 'trade',
// action: 'partial',
// symbol: 'btc-usdt',
// data: [
// {
// size: 0.05145,
// price: 41977.9,
// side: 'buy',
// timestamp: '2022-04-11T09:40:10.881Z'
// },
// ]
// }
//
// orderbook
//
// {
// topic: 'orderbook',
// action: 'partial',
// symbol: 'ltc-usdt',
// data: {
// bids: [
// [104.29, 5.2264],
// [103.86,1.3629],
// [101.82,0.5942]
// ],
// asks: [
// [104.81,9.5531],
// [105.54,0.6416],
// [106.18,1.4141],
// ],
// timestamp: '2022-04-11T10:37:01.227Z'
// },
// time: 1649673421
// }
//
// order
//
// {
// topic: 'order',
// action: 'insert',
// user_id: 155328,
// symbol: 'ltc-usdt',
// data: {
// symbol: 'ltc-usdt',
// side: 'buy',
// size: 0.05,
// type: 'market',
// price: 0,
// fee_structure: { maker: 0.1, taker: 0.1 },
// fee_coin: 'ltc',
// id: 'ce38fd48-b336-400b-812b-60c636454231',
// created_by: 155328,
// filled: 0.05,
// method: 'market',
// created_at: '2022-04-11T14:09:00.760Z',
// updated_at: '2022-04-11T14:09:00.760Z',
// status: 'filled'
// },
// time: 1649686140
// }
//
// balance
//
// {
// topic: 'wallet',
// action: 'partial',
// user_id: 155328,
// data: {
// eth_balance: 0,
// eth_available: 0,
// usdt_balance: 18.94344188,
// usdt_available: 18.94344188,
// ltc_balance: 0.00005,
// ltc_available: 0.00005,
// }
// }
//
if (!this.handleErrorMessage (client, message)) {
return;
}
const content = this.safeString (message, 'message');
if (content === 'pong') {
this.handlePong (client, message);
return;
}
const methods = {
'trade': this.handleTrades,
'orderbook': this.handleOrderBook,
'order': this.handleOrder,
'wallet': this.handleBalance,
'usertrade': this.handleMyTrades,
};
const topic = this.safeValue (message, 'topic');
const method = this.safeValue (methods, topic);
if (method !== undefined) {
method.call (this, client, message);
}
}
ping (client) {
// hollaex does not support built-in ws protocol-level ping-pong
return { 'op': 'ping' };
}
handlePong (client, message) {
client.lastPong = this.milliseconds ();
return message;
}
onError (client, error) {
this.options['ws-expires'] = undefined;
super.onError (client, error);
}
onClose (client, error) {
this.options['ws-expires'] = undefined;
super.onClose (client, error);
}
};