UNPKG

tlab-trading-toolkit

Version:

A trading toolkit for building advanced trading bots on the GDAX platform

457 lines (456 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ProductMap_1 = require("../ProductMap"); /*************************************************************************************************************************** * @license * * Copyright 2017 Coinbase, Inc. * * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * * with the License. You may obtain a copy of the License at * * * * http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * * License for the specific language governing permissions and limitations under the License. * ***************************************************************************************************************************/ const crypto = require("crypto"); const BitfinexCommon_1 = require("./BitfinexCommon"); const types_1 = require("../../lib/types"); const ExchangeFeed_1 = require("../ExchangeFeed"); /** * A client class exposing the Bitfinex public websocket feed * * The possible channels to subscribe to are: ticker, book, trades * * The raw feed is re-interpreted and emitted as POJOs rather than Bitfinex's array structures. * If StandardMessages is true, the following standard messages are emitted * ticker, snapshot, open, done, match * * The following events are emitted if standardMessages is false: * bitfinex-ticker: BitfinexTickerMessage * bitfinex-orderbook-snapshot: BitfinexOrderbookSnapshot * bitfinex-orderbook-update: BitfinexOrderMessage * bitfinex-trade-snapshot: BitfinexTradeSnapshot * bitfinex-trade-update: BitfinexTradeMessage * * The following operational messages are also emitted * close, error, open, connection * */ class BitfinexFeed extends ExchangeFeed_1.ExchangeFeed { constructor(config) { super(config); this.standardMessages = config.standardMessages || true; this.clearChannels(); this.counters = {}; this.snapshotDepth = config.snapshotDepth || 250; this.connect(); } get owner() { return 'Bitfinex'; } clearChannels() { this.subscriptions = {}; } /** * Resubscribe to channels using fire-and-forget. */ resubscribeAll() { for (const chanId in this.subscriptions) { const channel = this.subscriptions[chanId]; this.unsubscribe(chanId); this.subscribe(channel.type, channel.pair); this.removeSubscription({ chanId: channel.id }); } } nextSequence(product) { let counter = this.counters[product]; if (!counter) { counter = this.counters[product] = { base: -1, offset: 0 }; } if (counter.base < 1) { console.trace(`Requesting next sequence without setting snapshot sequence for product ${product}, current counter offset ${counter.offset}`); return -1; } counter.offset += 1; let seq = counter.base + counter.offset; return seq; } setSnapshotSequence(product, sequence) { let counter = this.counters[product]; if (!counter) { counter = this.counters[product] = { base: -1, offset: 0 }; } counter.base = sequence; } subscribe(channelType, product) { let subscribeMessage; switch (channelType) { case 'book': subscribeMessage = { event: 'subscribe', freq: 'F0', channel: 'book', pair: product, prec: BitfinexCommon_1.ORDERBOOK_PRECISION[product], len: this.snapshotDepth.toString() }; break; case 'ticker': case 'trades': subscribeMessage = { event: 'subscribe', channel: channelType, pair: product }; break; case 'auth': subscribeMessage = this.getAuthMessage(); if (!subscribeMessage) { return; } break; } this.send(subscribeMessage, (err) => { if (err) this.log('error', `Error subscribing to Bitfinex channel ${channelType} ${product}`, err); }); } unsubscribe(chanId) { const unsubscribeMessage = { event: 'unsubscribe', chanId: chanId }; this.send(unsubscribeMessage); } onOpen() { // set a pinger to keep the socket alive this.pinger = setInterval(() => { this.send({ event: 'ping' }); }, 120000); this.resubscribeAll(); } onClose(code, reason) { clearInterval(this.pinger); super.onClose(code, reason); } handleMessage(data) { const self = this; let msg; try { msg = JSON.parse(data); } catch (e) { this.log('warn', 'Invalid WS message from Bitfinex', { data: data }); return; } if (msg.event) { return handle_info_event(msg); } if (this.paused) { return; } if (!(Array.isArray(msg))) { this.log('warn', 'Unknown Bitfinex message type', { data: msg }); return; } const channelId = msg[0]; const channel = this.subscriptions[channelId]; if (!channel) { this.log('debug', 'Received message for unknown channel', msg); return; } // Heartbeat message if (msg[1] === 'hb') { self.subscriptions[msg[0]].lastHB = new Date(); return; } const type = channel.type; switch (type) { case 'ticker': return handle_ticker_message(msg); case 'book': return handle_book_message(msg); case 'trades': console.log(msg); return handle_trade_message(msg); case 'auth': return handle_auth_message(msg); default: this.log('warn', 'Unknown Bitfinex WS message type', msg); } function handle_info_event(message) { self.log('info', 'Info event from Bitfinex Websocket', message); if (message.code === 20060) { self.log('warn', 'Bitfinex is syncing trading engine. Pausing messages until they resume'); self.paused = true; return; } if (message.code === 20061) { // As per the Bitfinex API docs, we should un- and re-subscribe to all channels after an update self.log('info', 'Bitfinex syncing complete. Resuming message processing'); self.paused = false; self.resubscribeAll(); return; } if (message.event === 'subscribed') { self.addSubscription(message); return; } if (message.event === 'unsubscribed') { self.removeSubscription(message); return; } if (message.event === 'pong') { self.log('debug', 'Bitfinex WS feed: Pong!'); return; } if (message.version) { if (message.version !== BitfinexCommon_1.WEBSOCKET_API_VERSION) { const err = 'Error. Bitfinex websocket API version has changed to ' + message.version; self.log('info', err); self.emit('error', new Error(err)); } return; } self.log('info', 'Info channel on Bitfinex WS feed received an unknown message', message); } function handle_ticker_message(message) { if (message.length === 11) { const ticker = { channel_id: message[0], bid: message[1], bid_size: message[2], ask: message[3], ask_size: message[4], daily_change: message[5], daily_change_perc: message[6], last_price: message[7], volume: message[8], high: message[9], low: message[10] }; if (self.standardMessages) { self.push(self.mapTicker(ticker)); } else { self.push(ticker); } return; } self.log('warn', 'Ticker channel on Bitfinex WS feed received an unknown message', message); } function handle_book_message(message) { // Handle snapshot if (message.length === 2 && Array.isArray(message[1])) { self.log('info', 'Bitfinex orderbook snapshot received'); const snapshot = { channel_id: message[0], orders: message[1].map((order) => { return { price: order[0], count: parseInt(order[1], 10), size: order[2] }; }) }; if (self.standardMessages) { self.push(self.mapSnapshot(snapshot)); } else { self.push(snapshot); } return; } // Handle update if (message.length === 4) { const order = { channel_id: message[0], price: message[1], count: message[2], size: message[3] }; if (self.standardMessages) { const mappedMessage = self.mapOrderMessage(order); self.push(mappedMessage); } else { self.push(order); } return; } self.log('info', 'Orderbook channel on Bitfinex WS feed received an unknown message', { data: message }); } function handle_trade_message(message) { // Handle snapshot if (message.length === 2 && Array.isArray(message[1])) { self.log('info', 'Bitfinex trades snapshot received'); const snapshot = { channel_id: message[0], trades: message[1].map((trade) => { return { trade_id: trade[0], sequence: trade[0], timestamp: new Date(+trade[1] * 1000), price: trade[2], size: trade[3] }; }) }; self.push(snapshot); return; } // Handle update if (message.length >= 6) { const tu = message[1] === 'tu'; const trade = { channel_id: message[0], sequence: message[2], timestamp: new Date(+message[tu ? 4 : 3] * 1000), price: message[tu ? 5 : 4], size: message[tu ? 6 : 5] }; if (tu) { trade.trade_id = message[3]; } if (self.standardMessages) { self.push(self.mapTradeMessage(trade)); } else { self.push(trade); } return; } self.log('info', 'Orderbook channel on Bitfinex WS feed received an unknown message', { data: message }); } function handle_auth_message(message) { self.log('info', 'Auth messages are not supported yet', { data: message }); } } mapProduct(id) { return ProductMap_1.ProductMap.ExchangeMap.get('Bitfinex').getGenericProduct(id); } mapTicker(bt) { const pair = this.subscriptions[bt.channel_id].pair; const productId = this.mapProduct(pair); return { type: 'ticker', productId: productId, price: types_1.Big(bt.last_price), bid: types_1.Big(bt.bid), ask: types_1.Big(bt.ask), volume: types_1.Big(bt.volume), time: new Date(), trade_id: null, size: null }; } mapSnapshot(bs) { const pair = this.subscriptions[bs.channel_id].pair; console.log('Mapping snapshot for ', pair); this.setSnapshotSequence(pair, 1); const productId = this.mapProduct(pair); const bids = []; const asks = []; const orders = {}; bs.orders.forEach((order) => { const size = +order.size; if (size === 0) { return; } const newOrder = { id: order.price, price: types_1.Big(order.price), size: types_1.Big(order.size), side: size > 0 ? 'buy' : 'sell' }; const level = { price: types_1.Big(order.price), totalSize: types_1.Big(order.size).abs(), orders: [newOrder] }; if (size > 0) { bids.push(level); } else { asks.push(level); } orders[newOrder.id] = newOrder; }); return { sequence: this.nextSequence(pair), time: new Date(), type: 'snapshot', productId: productId, asks: asks, bids: bids, orderPool: orders }; } mapOrderMessage(order) { const pair = this.subscriptions[order.channel_id].pair; const productId = this.mapProduct(pair); let side = null; const size = +order.size; if (size < 0) { side = 'sell'; } if (size > 0) { side = 'buy'; } return { type: 'level', productId: productId, sequence: this.nextSequence(pair), time: new Date(), price: order.price, size: order.count === 0 ? '0' : Math.abs(size).toString(), side: side, count: order.count }; } mapTradeMessage(trade) { const pair = this.subscriptions[trade.channel_id].pair; const productId = this.mapProduct(pair); const size = +trade.size; const side = size < 0 ? 'sell' : 'buy'; return { type: 'trade', tradeId: trade.trade_id, time: trade.timestamp, productId: productId, price: trade.price, side: side, size: Math.abs(size).toString(), }; } addSubscription(msg) { this.log('info', `Subscribed to Bitfinex Websocket channel ${msg.channel} for ${msg.pair}`); this.subscriptions[msg.chanId] = { id: msg.chanId, type: msg.channel, pair: msg.pair, lastHB: null }; } removeSubscription(msg) { this.log('info', 'Unsubscribed from Bitfinex Websocket channel ' + msg.channel); const chanId = msg.chanId; delete this.subscriptions[chanId]; return chanId; } getAuthMessage() { if (!this.auth) { return null; } const authNonce = Date.now() * 1000; const authPayload = 'AUTH' + authNonce; const authSig = crypto.createHmac('sha384', this.auth.secret) .update(authPayload).digest('hex'); return { apiKey: this.auth.key, authSig, authNonce, authPayload, event: 'auth' }; } } exports.BitfinexFeed = BitfinexFeed;