Skip to content

Instantly share code, notes, and snippets.

@hitbtc-com
Created October 2, 2018 14:40
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save hitbtc-com/fc738c1b926d9d7aa7e3bd5247f792a1 to your computer and use it in GitHub Desktop.
Save hitbtc-com/fc738c1b926d9d7aa7e3bd5247f792a1 to your computer and use it in GitHub Desktop.
Socket API example with orderbook
if (typeof WebSocket !== 'function') {
// for node.js install ws package
WebSocket = require('ws');
}
const logger = {
debug: (...arg) => {
// console.log((new Date).toISOString(), 'DEBUG', ...arg)
},
info: (...arg) => {
console.log((new Date).toISOString(), 'INFO', ...arg)
},
warn: (...arg) => {
console.log((new Date).toISOString(), 'WARN', ...arg)
}
};
class SocketClient {
constructor(onConnected) {
this._id = 1;
this._createSocket();
this._onConnected = onConnected;
this._promises = new Map();
this._handles = new Map();
}
_createSocket() {
this._ws = new WebSocket('wss://api.hitbtc.com/api/2/ws');
this._ws.onopen = () => {
logger.info('ws connected');
this._onConnected();
};
this._ws.onclose = () => {
logger.warn('ws closed');
this._promises.forEach((cb, id) => {
this._promises.delete(id);
cb.reject(new Error('Disconnected'));
});
setTimeout(() => this._createSocket(), 500);
};
this._ws.onerror = err => {
logger.warn('ws error', err);
};
this._ws.onmessage = msg => {
logger.debug('<', msg.data);
try {
const message = JSON.parse(msg.data);
if (message.id) {
if (this._promises.has(message.id)) {
const cb = this._promises.get(message.id);
this._promises.delete(message.id);
if (message.result) {
cb.resolve(message.result);
} else if (message.error) {
cb.reject(message.error);
} else {
logger.warn('Unprocessed response', message)
}
}
} else if (message.method && message.params) {
if (this._handles.has(message.method)) {
this._handles.get(message.method).forEach(cb => {
cb(message.params);
});
} else {
logger.warn('Unprocessed method', message);
}
} else {
logger.warn('Unprocessed message', message);
}
} catch (e) {
logger.warn('Fail parse message', e);
}
}
}
request(method, params = {}) {
if (this._ws.readyState === WebSocket.OPEN) {
return new Promise((resolve, reject) => {
const requestId = ++this._id;
this._promises.set(requestId, {resolve, reject});
const msg = JSON.stringify({method, params, id: requestId});
logger.debug('>', msg);
this._ws.send(msg);
setTimeout(() => {
if (this._promises.has(requestId)) {
this._promises.delete(requestId);
reject(new Error('Timeout'));
}
}, 10000);
});
} else {
return Promise.reject(new Error('WebSocket connection not established'))
}
}
setHandler(method, callback) {
if (!this._handles.has(method)) {
this._handles.set(method, []);
}
this._handles.get(method).push(callback);
}
}
function updateIndex(sortedArray, item, index) {
if (index < sortedArray.length && sortedArray[index].price === item.price) {
if (item.size === 0) {
sortedArray.splice(index, 1);
} else {
sortedArray[index].size = item.size;
}
} else if (item.size !== 0) {
sortedArray.splice(index, 0, item);
}
return index === 0;
}
function getSortedIndex(array, value, inverse) {
inverse = Boolean(inverse);
let low = 0, high = array ? array.length : low;
while (low < high) {
let mid = (low + high) >>> 1;
if ((!inverse && (+array[mid].price < +value)) || (inverse && (+array[mid].price > +value))) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
class OrderBookStore {
constructor(onChangeBest) {
this._data = {};
this._onChangeBest = onChangeBest;
}
getOrderBook(symbol) {
return this._data[symbol.toString()];
}
snapshotOrderBook(symbol, ask, bid) {
this._data[symbol.toString()] = {
ask: ask,
bid: bid
};
}
updateOrderBook(symbol, ask, bid) {
const data = this._data[symbol.toString()];
if (data) {
let bestChanged = false;
ask.forEach(function (v) {
bestChanged |= updateIndex(data.ask, v, getSortedIndex(data.ask, v.price));
});
bid.forEach(function (v) {
bestChanged |= updateIndex(data.bid, v, getSortedIndex(data.bid, v.price, true));
});
if (bestChanged && this._onChangeBest) {
this._onChangeBest(symbol, data.ask.length > 0 ? data.ask[0].price : null, data.bid.length > 0 ? data.bid[0].price : null);
}
}
}
}
function generateRandom() {
let d = Date.now();
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
logger.info('Start application');
const orderBooks = new OrderBookStore((symbol, bestASk, bestBid) => {
logger.info('New best orderbook', symbol, bestASk, bestBid);
});
const socketApi = new SocketClient(async () => {
try {
const symbols = await socketApi.request('getSymbols');
const subscribeSymbols = symbols.filter(s => s.baseCurrency === 'BTC');
for (let s of subscribeSymbols) {
logger.info('Subscribe to orderbook', s.id);
await socketApi.request('subscribeOrderbook', {symbol: s.id});
}
// try to auth place your keys
await socketApi.request('login', {"algo": "BASIC",
"pKey": "3ef4a9f8c8bf04bd8f09884b98403eae",
"sKey": "2deb570ab58fd553a4ed3ee249fd2d51"});
const balance = await socketApi.request('getTradingBalance');
logger.info('balance',balance.filter(b => b.available !== '0' || b.reserved !== '0'));
// place order
await socketApi.request('subscribeReports');
await socketApi.request('newOrder', {clientOrderId: generateRandom(), symbol: 'BTCUSD', side: 'sell', price: '17777.00', quantity: '0.01'});
} catch (e) {
logger.warn(e);
}
});
socketApi.setHandler('snapshotOrderbook', params => {
orderBooks.snapshotOrderBook(params.symbol, params.ask, params.bid);
});
socketApi.setHandler('updateOrderbook', params => {
orderBooks.updateOrderBook(params.symbol, params.ask, params.bid);
});
@i--storm
Copy link

updateIndex compares string variable item.size to zero in strict mode
it should look like so:

function updateIndex(sortedArray, item, index) {
if (index < sortedArray.length && sortedArray[index].price === item.price) {
if (parseFloat(item.size) === 0) {
sortedArray.splice(index, 1);
} else {
sortedArray[index].size = item.size;
}
} else if (parseFloat(item.size) !== 0) {
sortedArray.splice(index, 0, item);
}
return index === 0;
}

@StasToken
Copy link

Why not do whatever always given snapshot, they know what they have always orders. And orders that have already been closed will not disappear

@fwiessner
Copy link

This code just does not seem to work right, i always end up with some parts of the orderbook not beeing updated anymore.

@StasToken
Copy link

This code just does not seem to work right, i always end up with some parts of the orderbook not beeing updated anymore.

I've been banging the brain and revealed the secret, they have closed orders given volume 0
It's completely idiotic, but I'll tell you how it works:

  • receive modified orders
  • sort them in ascending or descending order
  • after sorting, those orders that are value === 0 remove
  • order book update is finished!

don't forget to use the patch from i--storm which he pointed out above

PS: I don't know why these idiots did, this game full! but on the other to get the order book update will not work

@fwiessner
Copy link

Yes, i know that order volume 0 means to close the order, that is not the problem. The problem is after running a while, asks gets lower than bids or vice versa and one side never seems to get updated anymore. I use ccxws with this orderbook implementation here but it does not work quite well, mostly because the index is maintained as sometimes as float and sometime as string which is weird, it then cannot update the index if i got a price like 7700.230000 and an update like 7700.23 the update fails because it seems index is handled as a string?

@StasToken
Copy link

Why don't you use type conversion?

let string = '7700.230000'; // "7700.230000" let number = Number(string); // 7700.23

In General, all input data is converted to number and then there should be no problem...

@valamidev
Copy link

I made an NPM package based on the logic of this implementation and solve some issues what I discovered:

https://www.npmjs.com/package/orderbook-synchronizer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment