diff --git a/config_example.json b/config_example.json index 8cd417c..d08d140 100644 --- a/config_example.json +++ b/config_example.json @@ -68,10 +68,16 @@ } }, + "profitSwitch": { + "enabled": false, + "updateInterval": 60, + "depth": 0.80 + }, + "redisBlockNotifyListener": { "enabled": false, "redisPort": 6379, "redisHost": "hostname", "psubscribeKey": "newblocks:*" } -} \ No newline at end of file +} diff --git a/init.js b/init.js index 0daf3fd..95f5a99 100644 --- a/init.js +++ b/init.js @@ -12,6 +12,7 @@ var WorkerListener = require('./libs/workerListener.js'); var PoolWorker = require('./libs/poolWorker.js'); var PaymentProcessor = require('./libs/paymentProcessor.js'); var Website = require('./libs/website.js'); +var ProfitSwitch = require('./libs/profitSwitch.js'); var algos = require('stratum-pool/lib/algoProperties.js'); @@ -58,7 +59,7 @@ catch(e){ if (cluster.isWorker){ - + switch(process.env.workerType){ case 'pool': new PoolWorker(logger); @@ -69,6 +70,9 @@ if (cluster.isWorker){ case 'website': new Website(logger); break; + case 'profitSwitch': + new ProfitSwitch(logger); + break; } return; @@ -203,23 +207,19 @@ var startCoinswitchListener = function(portalConfig){ logger.debug('Master', 'Coinswitch', text); }); listener.on('switchcoin', function(message){ - var ipcMessage = {type:'blocknotify', coin: message.coin, hash: message.hash}; Object.keys(cluster.workers).forEach(function(id) { cluster.workers[id].send(ipcMessage); }); var ipcMessage = { - type:'switch', - coin: message.coin - }; + type:'switch', + coin: message.coin + }; Object.keys(cluster.workers).forEach(function(id) { cluster.workers[id].send(ipcMessage); }); - }); listener.start(); - - }; var startRedisBlockListener = function(portalConfig){ @@ -287,6 +287,28 @@ var startWebsite = function(portalConfig, poolConfigs){ }; +var startProfitSwitch = function(portalConfig, poolConfigs){ + + if (!portalConfig.profitSwitch.enabled){ + logger.error('Master', 'Profit', 'Profit auto switching disabled'); + return; + } + + var worker = cluster.fork({ + workerType: 'profitSwitch', + pools: JSON.stringify(poolConfigs), + portalConfig: JSON.stringify(portalConfig) + }); + worker.on('exit', function(code, signal){ + logger.error('Master', 'Profit', 'Profit switching process died, spawning replacement...'); + setTimeout(function(){ + startWebsite(portalConfig, poolConfigs); + }, 2000); + }); +}; + + + (function init(){ var poolConfigs = buildPoolConfigs(); @@ -305,4 +327,6 @@ var startWebsite = function(portalConfig, poolConfigs){ startWebsite(portalConfig, poolConfigs); + startProfitSwitch(portalConfig, poolConfigs); + })(); diff --git a/libs/apiPoloniex.js b/libs/apiPoloniex.js new file mode 100644 index 0000000..35f330c --- /dev/null +++ b/libs/apiPoloniex.js @@ -0,0 +1,212 @@ +var request = require('request'); +var nonce = require('nonce'); + +module.exports = function() { + 'use strict'; + + // Module dependencies + + // Constants + var version = '0.1.0', + PUBLIC_API_URL = 'http://poloniex.com/public', + PRIVATE_API_URL = 'https://poloniex.com/tradingApi', + USER_AGENT = 'npm-crypto-apis/' + version + + // Constructor + function Poloniex(key, secret){ + // Generate headers signed by this user's key and secret. + // The secret is encapsulated and never exposed + this._getPrivateHeaders = function(parameters){ + var paramString, signature; + + if (!key || !secret){ + throw 'Poloniex: Error. API key and secret required'; + } + + // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` + paramString = Object.keys(parameters).sort().map(function(param){ + return encodeURIComponent(param) + '=' + encodeURIComponent(parameters[param]); + }).join('&'); + + signature = crypto.createHmac('sha512', secret).update(paramString).digest('hex'); + + return { + Key: key, + Sign: signature + }; + }; + } + + // If a site uses non-trusted SSL certificates, set this value to false + Poloniex.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Poloniex.prototype = { + constructor: Poloniex, + + // Make an API request + _request: function(options, callback){ + if (!('headers' in options)){ + options.headers = {}; + } + + options.headers['User-Agent'] = USER_AGENT; + options.json = true; + options.strictSSL = Poloniex.STRICT_SSL; + + request(options, function(err, response, body) { + callback(err, body); + }); + + return this; + }, + + // Make a public API request + _public: function(parameters, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL, + qs: parameters + }; + + return this._request(options, callback); + }, + + // Make a private API request + _private: function(parameters, callback){ + var options; + + parameters.nonce = nonce(); + options = { + method: 'POST', + url: PRIVATE_API_URL, + form: parameters, + headers: this._getPrivateHeaders(parameters) + }; + + return this._request(options, callback); + }, + + + ///// + + + // PUBLIC METHODS + + getTicker: function(callback){ + var parameters = { + command: 'returnTicker' + }; + + return this._public(parameters, callback); + }, + + get24hVolume: function(callback){ + var parameters = { + command: 'return24hVolume' + }; + + return this._public(parameters, callback); + }, + + getOrderBook: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOrderBook', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + getTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._public(parameters, callback); + }, + + + ///// + + + // PRIVATE METHODS + + myBalances: function(callback){ + var parameters = { + command: 'returnBalances' + }; + + return this._private(parameters, callback); + }, + + myOpenOrders: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnOpenOrders', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + myTradeHistory: function(currencyA, currencyB, callback){ + var parameters = { + command: 'returnTradeHistory', + currencyPair: joinCurrencies(currencyA, currencyB) + }; + + return this._private(parameters, callback); + }, + + buy: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'buy', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + sell: function(currencyA, currencyB, rate, amount, callback){ + var parameters = { + command: 'sell', + currencyPair: joinCurrencies(currencyA, currencyB), + rate: rate, + amount: amount + }; + + return this._private(parameters, callback); + }, + + cancelOrder: function(currencyA, currencyB, orderNumber, callback){ + var parameters = { + command: 'cancelOrder', + currencyPair: joinCurrencies(currencyA, currencyB), + orderNumber: orderNumber + }; + + return this._private(parameters, callback); + }, + + withdraw: function(currency, amount, address, callback){ + var parameters = { + command: 'withdraw', + currency: currency, + amount: amount, + address: address + }; + + return this._private(parameters, callback); + } + }; + + return Poloniex; +}(); diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js new file mode 100644 index 0000000..be743c9 --- /dev/null +++ b/libs/profitSwitch.js @@ -0,0 +1,227 @@ +var async = require('async'); + +var Poloniex = require('./apiPoloniex.js'); + +module.exports = function(logger){ + + var _this = this; + + var portalConfig = JSON.parse(process.env.portalConfig); + var poolConfigs = JSON.parse(process.env.pools); + + var logSystem = 'Profit'; + + // + // build status tracker for collecting coin market information + // + var profitStatus = {}; + var profitSymbols = {}; + Object.keys(poolConfigs).forEach(function(coin){ + + var poolConfig = poolConfigs[coin]; + var algo = poolConfig.coin.algorithm; + + if (!profitStatus.hasOwnProperty(algo)) { + profitStatus[algo] = {}; + } + var coinStatus = { + name: poolConfig.coin.name, + symbol: poolConfig.coin.symbol, + difficulty: 0, + reward: 0, + avgPrice: { BTC: 0, LTC: 0 }, + avgDepth: { BTC: 0, LTC: 0 }, + avgVolume: { BTC: 0, LTC: 0 }, + prices: {}, + depths: {}, + volumes: {}, + }; + profitStatus[algo][poolConfig.coin.symbol] = coinStatus; + profitSymbols[poolConfig.coin.symbol] = algo; + }); + + + // + // ensure we have something to switch + // + var isMoreThanOneCoin = false; + Object.keys(profitStatus).forEach(function(algo){ + if (Object.keys(profitStatus[algo]).length > 1) { + isMoreThanOneCoin = true; + } + }); + if (!isMoreThanOneCoin){ + logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); + return; + } + logger.debug(logSystem, 'profitStatus', JSON.stringify(profitStatus)); + logger.debug(logSystem, 'profitStatus', JSON.stringify(profitSymbols)); + + + // + // setup APIs + // + var poloApi = new Poloniex( + // 'API_KEY', + // 'API_SECRET' + ); + + // + // market data collection from Poloniex + // + this.getProfitDataPoloniex = function(callback){ + async.series([ + function(taskCallback){ + poloApi.getTicker(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcPrice = new Number(0); + var ltcPrice = new Number(0); + + if (data.hasOwnProperty('BTC_' + symbol)) { + btcPrice = new Number(data['BTC_' + symbol]); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcPrice = new Number(data['LTC_' + symbol]); + } + + if (btcPrice > 0 || ltcPrice > 0) { + var prices = { + BTC: btcPrice, + LTC: ltcPrice + }; + profitStatus[profitSymbols[symbol]][symbol].prices['Poloniex'] = prices; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + poloApi.get24hVolume(function(err, data){ + if (err){ + taskCallback(err); + return; + } + Object.keys(profitSymbols).forEach(function(symbol){ + var btcVolume = new Number(0); + var ltcVolume = new Number(0); + + if (data.hasOwnProperty('BTC_' + symbol)) { + btcVolume = new Number(data['BTC_' + symbol].BTC); + } + if (data.hasOwnProperty('LTC_' + symbol)) { + ltcVolume = new Number(data['LTC_' + symbol].LTC); + } + + if (btcVolume > 0 || ltcVolume > 0) { + var volumes = { + BTC: btcVolume, + LTC: ltcVolume + }; + profitStatus[profitSymbols[symbol]][symbol].volumes['Poloniex'] = volumes; + } + }); + taskCallback(); + }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(profitSymbols).forEach(function(symbol){ + var coinVolumes = profitStatus[profitSymbols[symbol]][symbol].volumes; + var coinPrices = profitStatus[profitSymbols[symbol]][symbol].prices; + + if (coinVolumes.hasOwnProperty('Poloniex') && coinPrices.hasOwnProperty('Poloniex')){ + var btcDepth = new Number(0); + var ltcDepth = new Number(0); + + if (coinVolumes['Poloniex']['BTC'] > 0 && coinPrices['Poloniex']['BTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['BTC']); + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('BTC', symbol, coinPrice, callback) + }); + } + if (coinVolumes['Poloniex']['LTC'] > 0 && coinPrices['Poloniex']['LTC'] > 0){ + var coinPrice = new Number(coinPrices['Poloniex']['LTC']); + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('LTC', symbol, coinPrice, callback) + }); + } + } + }); + + if (depthTasks.length == 0){ + taskCallback; + return; + } + async.parallel(depthTasks, function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + + }; + this.getMarketDepthFromPoloniex = function(symbolA, symbolB, coinPrice, callback){ + poloApi.getOrderBook(symbolA, symbolB, function(err, data){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + if (data.hasOwnProperty('bids')){ + data['bids'].forEach(function(order){ + var price = new Number(order[0]); + var qty = new Number(order[1]); + // only measure the depth down to configured depth + if (price >= coinPrice * portalConfig.profitSwitch.depth){ + depth += (qty * price); + } + }); + } + + if (!profitStatus[profitSymbols[symbolB]][symbolB].depths.hasOwnProperty('Poloniex')){ + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'] = { + BTC: 0, + LTC: 0 + }; + } + profitStatus[profitSymbols[symbolB]][symbolB].depths['Poloniex'][symbolA] = depth; + callback(); + }); + }; + + // TODO + this.getProfitDataCryptsy = function(callback){ + callback(null); + }; + + + var checkProfitability = function(){ + logger.debug(logSystem, 'Check', 'Running profitability checks.'); + + async.parallel([ + _this.getProfitDataPoloniex, + _this.getProfitDataCryptsy + ], function(err){ + if (err){ + logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); + return; + } + logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); + }); + }; + setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); + +}; diff --git a/package.json b/package.json index c1a28a3..2140f2c 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,11 @@ "compression": "*", "dot": "*", "colors": "*", - "node-watch": "*" + "node-watch": "*", + "request": "*", + "nonce": "*" }, "engines": { "node": ">=0.10" } -} \ No newline at end of file +}