UNPKG

crypto-nodes

Version:

969 lines (642 loc) 21.8 kB
const Gdax = require('gdax'); var request = require('request'); const https = require('https'); var uuid = require('uuid'); var tls = require('tls'); var fix = require('fixjs'); var crypto = require('crypto'); var exchange = 'gdax'; var per_second = 0; var connected = false; var no_data = 0; var max_no_data = 5; // wrapper for GDAX FIX var gdax_fix = { uuid_map: {}, init: function (node, config) { this.node = node; this.config = config; this.connect(); }, connect: function() { var that = this; if(!this.config.server_fix || !this.config.server_fix_port) { that.node.status({ fill: 'red', shape:'ring', text: "Missing FIX Config" }); return; } if(!this.config.api_key || !this.config.api_secret || !this.config.api_pass) { that.node.status({ fill: 'red', shape:'ring', text: "Missing FIX Auth" }); return; } this.stream = tls.connect({ host: 'fix-public.sandbox.gdax.com', port: '4198' }, function() { that.node.status({ fill: 'green', shape:'ring', text: "FIX Connected" }); }); this.client = fix.createClient(this.stream); this.session = this.client.session(this.config.api_key, 'Coinbase'); // Stream Events // this.stream.on('data', function(data) { console.log("FIX DATA :: " + data.toString()); }); this.stream.on('error', function(err) { console.log("FIX ERR :: " + err); }); this.stream.on('close', function () { console.log("FIX DISCONNECT"); }); this.stream.on('end', function() { console.log('FIX STREAM ENDED'); }); // Session Events this.session.on('send', function(msg) { /*console.log('sending message: %s', msg);*/ }); this.session.on('error', function(err) { console.error(err.stack); }); this.session.on('logout', function() { console.log('FIX SESSION :: Logged out'); }); this.session.on('end', function() { console.log('FIX SESSION :: Ended'); /*this.stream.end();*/ }); this.session.on('logon', function() { that.node.status({ fill: 'green', shape:'ring', text: "FIX Authenticated" }); }); // this.session.on('Reject', function(msg, next) { console.log('reject: %s', msg); next(); }); // this.session.on('OrderCancelReject', function(msg, next) { console.log('order cancel reject: %s', msg); next(); }); // The rest of these are handlers for various FIX keywords this.session.on('ExecutionReport', function(msg, next) { that.node.status({ fill: 'green', shape:'ring', text: "FIX Order Report" }); // console.log('FIX ORDER DATA :: ' + msg.toString()); var fnk_map = { '0': 'Added', '1': 'Filled', '3': 'Done', '4': 'Canceled', '7': 'Stopped', '8': 'Rejected', 'D': 'Changed', 'I': 'Info' }; // console.log(msg.ExecType); switch(msg.ExecType) { case '0': // NEW if(that.uuid_map[msg.ClOrdID]) { // Create mapping between REMOTE ORDER ID and LOCAL ORDER ID that.uuid_map[msg.OrderID] = that.uuid_map[msg.ClOrdID]; var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'ADDED', msg: 'Order Added', data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); } break; case '1': // FILL var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'FILLED', filled_amount: parseFloat(msg.LastShares), msg: 'Order Filled', data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); break; case '3': // DONE var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'DONE', msg: 'Order Done', data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); break; case '4': // CANCELED var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'CANCEL', msg: 'Order Canceled', data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); break; case '7': // STOPPED var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'CANCEL', msg: 'Order Stopped', data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); break; case '8': // REJECTED if(that.uuid_map[msg.ClOrdID]) { // Create mapping between REMOTE ORDER ID and LOCAL ORDER ID that.uuid_map[msg.OrderID] = that.uuid_map[msg.ClOrdID]; } var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'ERROR', msg: (msg.OrdRejReason && msg.OrdRejReason == 3 ? 'FUNDS': 'OTHER'), data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); break; case 'D': // CHANGED break; case 'I': // STATUS var order_log_obj = { op: 'order_log', order_id: that.uuid_map[msg.OrderID], status: 'INFO', msg: 'Order Info', data: { exchange: 'gdax', protocol: 'fix', data: msg.toString() } } that.node.send({ payload: order_log_obj }); break; } //if(fnk_map[msg.ExecType]) { if(that.uuid_map[msg.OrderID]) { var text = 'Order ' + fnk_map[msg.ExecType] + '! Local ID ' + that.uuid_map[msg.OrderID] || ' NOT FOUND'; console.log(text); that.node.status({ fill: 'green', shape:'ring', text: text }); } //} next(); }); // DO LOGIN :: var logon = new fix.Msgs.Logon(); logon.SendingTime = new Date(); logon.HeartBtInt = 30; logon.EncryptMethod = 0; logon.set(554, this.config.api_pass); // FIX 4.4 Password tag var presign = [ logon.SendingTime, logon.MsgType, this.session.outgoing_seq_num, this.session.sender_comp_id, this.session.target_comp_id, this.config.api_pass ]; var what = presign.join('\x01'); logon.RawData = this.sign(what, this.config.api_secret); // console.log(logon); this.session.send(logon, true); }, status: function () { var orders = new fix.Msgs.OrderStatusRequest(); orders.OrderID = '*'; this.session.send(orders); }, sign: function(what, secret) { var key = Buffer(secret, 'base64'); var hmac = crypto.createHmac('sha256', key); //console.log("presign: " + what); var signature = hmac.update(what).digest('base64'); return signature; }, sendOrder(symbol, price, amount, is_buy, id) { var order = new fix.Msgs.NewOrderSingle(); var order_uuid = uuid(); // Map our numeric ID's to UUID to send to order this.uuid_map[order_uuid] = id; // Populate order object order.Symbol = symbol; order.ClOrdID = order_uuid; order.Side = is_buy ? 1 : 2; order.HandlInst = 1; order.OrdType = 2; // 2=Limit order.OrderQty = amount; order.Price = price; order.TimeInForce = '3'; // 1=GTC 3=IOC order.TransactTime = new Date(); order.set(7928, 'D'); // STP this.session.send(order); }, cancelOrder: function(ClOrdID, OrderID, symbol) { var cancel = new fix.Msgs.OrderCancelRequest(); cancel.Symbol = symbol; cancel.OrigClOrdID = ClOrdID; cancel.ClOrdID = 123456; cancel.OrderID = OrderID; this.session.send(cancel); } } var gdax_obj = { init_ws: function(node, symbols) { if(this.gdax_ws && this.gdax_ws.socket && (this.gdax_ws.socket.readyState == 1 || this.gdax_ws.socket.readyState == 2)) { console.log('Already have socket..'); return; } var that = this; //console.log(this.gdax_ws_addr); //console.log(this.gdax_ws_auth); this.gdax_ws = new Gdax.WebsocketClient(symbols, this.gdax_ws_addr, this.gdax_ws_auth, this.gdax_ws_opts); this.gdax_ws.on('open', (data) => { console.log('GDAX :: CONNECTED'); // connected = true; }); this.gdax_ws.on('message', (data) => { if(!data.product_id) { //console.log(data); if(data.type == 'subscriptions') { // console.log(data.channels[0].product_ids); } return; } switch(data.type) { case 'user': console.log('USER DATA::'); console.log(data); break; case 'heartbeat': //console.log('HB'); break; case 'snapshot': // console.log('SNAPSHOT!!'); case 'l2update': //console.log('.'); var sym_from = data.product_id.substr(0, data.product_id.indexOf("-")); var sym_to = data.product_id.substr(data.product_id.indexOf("-") + 1, data.product_id.length - data.product_id.indexOf("-") + 1); var payload = { exchange: 'gdax', fees: this.fees, sym_from: sym_from, sym_to: sym_to, data: data }; node.send({ payload: payload }); per_second++; break; default: console.log('GDAX ::'); console.log(data); break; } }); this.gdax_ws.on('error', err => { if(err.message.indexOf('Failed to initialize level 2 channel for ') > -1) { //console.log(err); function resub(sym) { setTimeout(function () { //console.log('GDAX RESUB :: ' + sym); that.gdax_ws.subscribe({ product_ids: [ sym ], channels: ['level2'] }); }, 5000); } var sym = err.message.substr(41, 7); resub(sym); } else { console.log('GDAX ::'); console.log(err); } }); this.gdax_ws.on('close', () => { console.log('GDAX :: WebSocket connection disconnected'); setTimeout(function () { // that.gdax_ws.disconnect(); setTimeout(function () { that.init_ws(node, symbols); }, 1); }, 5000); }); setTimeout(function () { console.log('RESET ::'); that.gdax_ws.disconnect(); setTimeout(function () { that.init_ws(node, symbols); }, 1); }, 60000); }, exchange_symbols: [], configured_symbols: [], enabled_symbols: [], get_enabled_symbols: function() { var out = []; for(x in this.exchange_symbols) { var pass = false; var exc_tmp = this.exchange_symbols[x].split('-'); for(y in this.configured_symbols) { var cfg_tmp = this.configured_symbols[y].split('/'); // If its matching or inverted variant matches its enabled symbol if((exc_tmp[0] == cfg_tmp[0] && exc_tmp[1] == cfg_tmp[1]) || (exc_tmp[0] == cfg_tmp[1] && exc_tmp[1] == cfg_tmp[0]) ) { pass = true; } } if(pass) { out.push(this.exchange_symbols[x]); } } return out; }, msg: function(node, sym, ob) { var sym_from = sym.substr(0,3); var sym_to = sym.substr(3); per_second++; node.send({ payload: { 'exchange' : 'gdax', fees: this.fees, sym_from: sym_from, sym_to: sym_to, 'data' : ob } }); }, init: function(config, node, symbols) { console.log(config); var that = this; try { this.fees = JSON.parse(config.fees); } catch (e) {} this.configured_symbols = symbols; this.gdax_ws_opts = { "channels": ['level2', 'user'] }; if(config.api_key && config.api_secret && config.api_pass) { this.gdax_ws_auth = { key: config.api_key, secret: config.api_secret, passphrase: config.api_pass }; // Client this.authedClient = new Gdax.AuthenticatedClient( config.api_key, config.api_secret, config.api_pass, config.server_api ); } else { this.authedClient = new Gdax.PublicClient(config.server_api); this.gdax_ws_auth = null; } this.gdax_rq_opts = { url: config.server_api + '/products', headers: { 'User-Agent': '*' }}; this.gdax_ws_addr = config.server_ws; if(this.gdax_ws && this.gdax_ws.socket) { this.gdax_ws.disconnect(); } if (!this.interval) { this.interval = setInterval(function() { var color = per_second > 0 ? 'green' : 'red'; node.status({ fill: color, shape: 'ring', text: per_second + ' requests per second' }); per_second = 0; }, 1000); } this.get_exchange_symbols(function (exchange_symbols) { that.exchange_symbols = exchange_symbols; var enabled_symbols = that.get_enabled_symbols(); //console.log('ENABLED::'); //console.log(enabled_symbols); //console.log(that.configured_symbols); that.init_ws(node, enabled_symbols); }); }, symbols_map: {}, invert_map: {}, get_exchange_symbols(callback) { request(this.gdax_rq_opts, function(error, response, body) { if(body) { var currencies = []; try { body = JSON.parse(body); for (var i = 0; i < body.length; i++) { currencies.push(body[i].id); } } catch(e) { } callback(currencies); } else { callback([]); } }); }, place_order: function(msg, obj, node) { authedClient.placeOrder(obj, function (o_err, o_res, o_data) { console.log('GDAX ORDER RESULT::'); console.log(o_data); if(o_err) { if(o_err.response.statusCode == 400 && o_err.response.body == '{"message":"Insufficient funds"}') { node.status({ fill: 'red',shape:'ring', text: "Insufficient funds" }); var order_log_obj = { op: 'order_log', order_id: msg.payload.tx.acc, status: 'ERROR', msg: 'ORDER ERROR :: FUNDS', data: { response: o_err.response.body, tx: msg.payload.tx } } node.send({ payload: order_log_obj }); } else { var err = 'ORDER ERROR :: ' + o_err.response.statusCode + ' ' + o_err.response.statusMessage + ' ' + o_err.response.body; node.status({ fill: 'red',shape:'ring', text: err }); var order_log_obj = { op: 'order_log', order_id: msg.payload.tx.acc, status: 'ERROR', msg: err, data: { response: o_err.response.body, tx: msg.payload.tx } } node.send({ payload: order_log_obj }); } return; } if(o_data.id && o_data.status == 'pending') { node.send({ payload: { op: 'exchange_order', status: 'accepted', order: o_data, tx: msg.payload.tx } }); node.status({ fill: 'gray',shape:'ring', text: 'Order Pending' }); authedClient.getFills({ order_id: o_data.id }, function (f_err, f_res, f_data) { if(f_err) { var err = f_err.response.statusCode + ' ' + f_err.response.statusMessage + ' ' + f_err.response.body; node.status({ fill: 'red',shape:'ring', text: err }); var order_log_obj = { op: 'order_log', order_id: msg.payload.tx.acc, status: 'ERROR', msg: 'FILL ERROR :: ' + err, data: { response: o_err.response.body, tx: msg.payload.tx, order: o_data } } node.send({ payload: order_log_obj }); return; } if(f_data && f_data.length) { console.log('GDAX FILL RESULT::'); console.log(f_data); node.send({ payload: { op: 'exchange_order', status: 'fills', fills: f_data, tx: msg.payload.tx } }); var order_log_obj = { op: 'order_log', order_id: msg.payload.tx.acc, status: 'FILL', msg: 'ORDER FILL :: ' + JSON.stringify(f_data), data: { response: o_err.response.body, tx: msg.payload.tx, order: o_data, fills: f_data } } node.send({ payload: order_log_obj }); node.status({ fill: 'orange',shape:'ring', text: 'Order Fill Data Received' }); } authedClient.getOrder(o_data.id, function (c_err, c_res, c_data) { node.status({ fill: 'green',shape:'ring', text: 'Order Checked' }); if(c_err) { var err = f_err.response.statusCode + ' ' + f_err.response.statusMessage + ' ' + f_err.response.body; var order_log_obj = { op: 'order_log', order_id: msg.payload.tx.acc, status: 'ERROR', msg: 'ORDER CHECK ERROR :: ' + c_err, data: { response: o_err.response.body, tx: msg.payload.tx, order: o_data } } node.send({ payload: order_log_obj }); return; } console.log('GDAX ORDER CHECK RESULT::'); console.log(c_data); node.send({ payload: { op: 'exchange_order', status: 'update', order: c_data, tx: msg.payload.tx } }); }); }); } }); }, } // VARS var interval; var symbols; module.exports = function(RED) { /* function gdax_start(node, fees) { gdax_get_symbols(function (symbols) { if(symbols) { console.log('GDAX :: SYMBOLS :: ' + symbols.join(',')); gdax_websocket_connect(node, symbols, fees); } else { // clearInterval(interval); node.status({ fill: 'red', shape: 'ring', text: 'Error receiving symbols' }); setTimeout(function () { gdax_start(node, fees); }, 1000); } }); } */ function gdaxConnector(config) { RED.nodes.createNode(this, config); var node = this; node.on('input', function(msg) { if(config.disabled) { // Disable UI Updates if(gdax_obj.interval) { clearInterval(gdax_obj.interval); } // Set node status node.status({ fill: 'red',shape:'ring', text: 'Disabled' }); // Emit flush orderbook node.send({payload: { exchange: 'binance', op: 'flush' }}); return; } if(msg.payload && msg.payload.op == 'subscribe') { gdax_obj.init(config, node, msg.payload.symbols); } }); node.on('close', function() { // tidy up any async code here - shutdown connections and so on. node.status({ fill: 'red',shape:'ring', text: 'Offline' }); node.send({payload: { exchange: 'gdax', op: 'flush' }}); }); } function gdaxOrder(config) { RED.nodes.createNode(this,config); var node = this; // Initialize our FIX library gdax_fix.init(node, config); node.on('input', function(msg) { // Basic validate its an exchange order for this exchange if(msg.payload && msg.payload.op && msg.payload.op == 'exchange_order' && msg.payload.exchange == exchange && msg.payload.tx) { console.log('GDAX SENDING EXCHANGE ORDER ::'); console.log(msg.payload.tx); gdax_fix.sendOrder( msg.payload.tx.symbol.replace('/','-'), msg.payload.tx.price + 5, msg.payload.tx.qty, msg.payload.tx.is_buy, msg.payload.tx.acc ); /* console.log('GDAX SENDING EXCHANGE ORDER ::'); console.log(msg.payload.tx); var obj = { type: 'limit', time_in_force: 'IOC', side: msg.payload.tx.is_buy ? "buy" : "sell", price: msg.payload.tx.price, size: msg.payload.tx.qty, product_id: msg.payload.tx.symbol.replace('/', '-'), client_oid: msg.payload.tx.acc, } */ } }); } function gdaxBalance(config) { RED.nodes.createNode(this,config); var node = this; this.conf = RED.nodes.getNode(config.config); // console.log(this.conf); //client.addStream(false, 'wallet', function (data, symbol, tableName) { // node.send({ payload: { exchange: exchange, op: 'get_balance', balances: out } }); // node.status({ fill: 'green',shape:'ring', text: text }); //}); node.on('input', function(msg) { if(msg.payload && msg.payload.op && msg.payload.op == 'get_balance') { if(msg.payload.exchange && (msg.payload.exchange == 'all' || msg.payload.exchange == exchange)) { node.conf.balance(function (err, out) { console.log('GDAX GET BALANCE ::'); console.log(out); node.send({ payload: { exchange: exchange, op: 'get_balance', balances: out } }); var text = ''; for(x in out) { text += x + ' = ' + out[x].balance + ', '; } text = text.substr(0, text.length - 2); node.status({ fill: 'green',shape:'ring', text: 'Received balance data ' + text }); }); } } }); } function gdaxConfig(n) { RED.nodes.createNode(this,n); var node = this; // Copy config to node var f = [ 'api_key','api_secret','api_pass','server_ws','server_api','server_fix','server_fix_port' ]; for(x in f) { node[f[x]] = n[f[x]]; } if(this.api_key && this.api_secret && this.api_pass && this.server_api) { this.authedClient = new Gdax.AuthenticatedClient( this.api_key, this.api_secret, this.api_pass, this.server_api ); } this.balance = function(callback) { var out = {}; if(node.authedClient) { node.authedClient.getCoinbaseAccounts(function (err, res, data) { if(err) { callback(err, out); } else { if(data && data.length) { for(x in data) { if(data[x].active && data[x].type == 'wallet') { out[data[x].currency] = { balance: data[x].balance }; // .available, .balance , .hold } } callback(null, out); } else { callback('GDAX - Bad balance data', out); } } }); } else { callback('GDAX - No Client', out); } } } RED.nodes.registerType("gdaxConfig", gdaxConfig); RED.nodes.registerType("gdaxBalance", gdaxBalance); RED.nodes.registerType("gdaxOrder", gdaxOrder); RED.nodes.registerType("gdaxConnector", gdaxConnector); }