From 0af27316d3abb08b70f1e85ec3bac596dea7af1a Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 13 Apr 2014 16:44:12 +0000 Subject: [PATCH 01/12] profitability data collection from Poloniex --- config_example.json | 8 +- init.js | 40 ++++++-- libs/apiPoloniex.js | 212 ++++++++++++++++++++++++++++++++++++++++ libs/profitSwitch.js | 227 +++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- 5 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 libs/apiPoloniex.js create mode 100644 libs/profitSwitch.js 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 +} From 550b09767e779d06995268664295316af44fb4b7 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 13 Apr 2014 16:46:06 +0000 Subject: [PATCH 02/12] whitespace formatting --- config_example.json | 8 +- libs/profitSwitch.js | 354 +++++++++++++++++++++---------------------- 2 files changed, 181 insertions(+), 181 deletions(-) diff --git a/config_example.json b/config_example.json index d08d140..0ddaa74 100644 --- a/config_example.json +++ b/config_example.json @@ -68,11 +68,11 @@ } }, - "profitSwitch": { - "enabled": false, + "profitSwitch": { + "enabled": false, "updateInterval": 60, - "depth": 0.80 - }, + "depth": 0.80 + }, "redisBlockNotifyListener": { "enabled": false, diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index be743c9..3de8b9c 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -11,217 +11,217 @@ module.exports = function(logger){ var logSystem = 'Profit'; - // - // build status tracker for collecting coin market information - // - var profitStatus = {}; - var profitSymbols = {}; + // + // 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; + 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; - }); + 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)); + // + // 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( + // + // setup APIs + // + var poloApi = new Poloniex( // 'API_KEY', - // 'API_SECRET' - ); + // 'API_SECRET' + ); - // - // market data collection from Poloniex - // + // + // 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); + 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 (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 (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 (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){ + 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.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){ + 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){ + }); + } + 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); - }); - + 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; - } + 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 (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; + 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 + // TODO this.getProfitDataCryptsy = function(callback){ - callback(null); + callback(null); }; var checkProfitability = function(){ - logger.debug(logSystem, 'Check', 'Running profitability checks.'); + 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)); - }); - }; + 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); }; From 93274f907b84010a0dff01aeb840a28c2bb3f039 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sun, 13 Apr 2014 23:17:40 +0000 Subject: [PATCH 03/12] added support for difficulty and blockreward --- libs/profitSwitch.js | 344 +++++++++++++++++++++++++------------------ 1 file changed, 198 insertions(+), 146 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 3de8b9c..7d57fd7 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,6 +1,7 @@ var async = require('async'); var Poloniex = require('./apiPoloniex.js'); +var Stratum = require('stratum-pool'); module.exports = function(logger){ @@ -19,25 +20,22 @@ module.exports = function(logger){ Object.keys(poolConfigs).forEach(function(coin){ var poolConfig = poolConfigs[coin]; - var algo = poolConfig.coin.algorithm; + 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; + if (!profitStatus.hasOwnProperty(algo)) { + profitStatus[algo] = {}; + } + var coinStatus = { + name: poolConfig.coin.name, + symbol: poolConfig.coin.symbol, + difficulty: 0, + reward: 0, + prices: {}, + depths: {}, + volumes: {}, + }; + profitStatus[algo][poolConfig.coin.symbol] = coinStatus; + profitSymbols[poolConfig.coin.symbol] = algo; }); @@ -46,13 +44,13 @@ module.exports = function(logger){ // var isMoreThanOneCoin = false; Object.keys(profitStatus).forEach(function(algo){ - if (Object.keys(profitStatus[algo]).length > 1) { - isMoreThanOneCoin = true; - } + 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, '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)); @@ -63,143 +61,143 @@ module.exports = function(logger){ // var poloApi = new Poloniex( // 'API_KEY', - // 'API_SECRET' + // '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); + 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 (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 (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 (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){ + 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.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){ + 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){ + }); + } + 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){ + if (depthTasks.length == 0){ + taskCallback; + return; + } + async.parallel(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); + } + ], function(err){ + if (err){ callback(err); - return; - } - callback(null); - }); - + return; + } + callback(null); + }); + }; this.getMarketDepthFromPoloniex = function(symbolA, symbolB, coinPrice, callback){ - poloApi.getOrderBook(symbolA, symbolB, function(err, data){ - if (err){ - callback(err); - return; - } + 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 (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; + 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 @@ -207,20 +205,74 @@ module.exports = function(logger){ callback(null); }; + this.getCoindDaemonInfo = function(callback){ + var daemonTasks = []; + Object.keys(profitStatus).forEach(function(algo){ + Object.keys(profitStatus[algo]).forEach(function(symbol){ + var coinName = profitStatus[algo][symbol].name; + var poolConfig = poolConfigs[coinName]; + var daemonConfig = poolConfig.shareProcessing.internal.daemon; + daemonTasks.push(function(callback){ + _this.getDaemonInfoForCoin(symbol, daemonConfig, callback) + }); + }); + }); + + if (daemonTasks.length == 0){ + callback(); + return; + } + async.parallel(daemonTasks, function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getDaemonInfoForCoin = function(symbol, cfg, callback){ + var daemon = new Stratum.daemon.interface([cfg]); + daemon.once('online', function(){ + daemon.cmd('getdifficulty', null, function(result){ + if (result[0].error != null){ + callback(result[0].error); + return; + } + profitStatus[profitSymbols[symbol]][symbol].difficulty = result[0].response; + + daemon.cmd('getblocktemplate', + [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], + function(result){ + if (result[0].error != null){ + callback(result[0].error); + return; + } + profitStatus[profitSymbols[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); + }); + callback(null) + }); + }).once('connectionFailed', function(error){ + callback(error); + }).on('error', function(error){ + callback(error); + }).init(); + }; + var checkProfitability = function(){ - logger.debug(logSystem, 'Check', 'Running profitability checks.'); + logger.debug(logSystem, 'Check', 'Running mining profitability check.'); - async.parallel([ - _this.getProfitDataPoloniex, - _this.getProfitDataCryptsy - ], function(err){ - if (err){ + async.parallel([ + _this.getProfitDataPoloniex, + _this.getProfitDataCryptsy, + _this.getCoindDaemonInfo + ], function(err){ + if (err){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); - return; - } + return; + } logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); - }); + }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 384a10d82548da9f16df456c212436bdc3914e92 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 01:52:57 +0000 Subject: [PATCH 04/12] checkpoint --- libs/apiCryptsy.js | 204 +++++++++++++++++++++++++++++++++++++++++++++ libs/apiMintpal.js | 204 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 libs/apiCryptsy.js create mode 100644 libs/apiMintpal.js diff --git a/libs/apiCryptsy.js b/libs/apiCryptsy.js new file mode 100644 index 0000000..6563ab5 --- /dev/null +++ b/libs/apiCryptsy.js @@ -0,0 +1,204 @@ +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://pubapi.cryptsy.com/api.php', + PRIVATE_API_URL = 'https://api.cryptsy.com/api', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Cryptsy(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 'Cryptsy: 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 + Cryptsy.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Cryptsy.prototype = { + constructor: Cryptsy, + + // 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 = Cryptsy.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 = { + method: 'marketdatav2' + }; + + 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 Cryptsy; +}(); diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js new file mode 100644 index 0000000..6563ab5 --- /dev/null +++ b/libs/apiMintpal.js @@ -0,0 +1,204 @@ +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://pubapi.cryptsy.com/api.php', + PRIVATE_API_URL = 'https://api.cryptsy.com/api', + USER_AGENT = 'nomp/node-open-mining-portal' + + // Constructor + function Cryptsy(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 'Cryptsy: 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 + Cryptsy.STRICT_SSL = true; + + // Helper methods + function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; + } + + // Prototype + Cryptsy.prototype = { + constructor: Cryptsy, + + // 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 = Cryptsy.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 = { + method: 'marketdatav2' + }; + + 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 Cryptsy; +}(); From 9ad11516d2aef63e69db39fb54f5b70a8c0118ea Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 01:53:40 +0000 Subject: [PATCH 05/12] checkpoint --- libs/apiPoloniex.js | 2 +- libs/poolWorker.js | 38 +++---- libs/profitSwitch.js | 262 ++++++++++++++++++++++++++----------------- 3 files changed, 180 insertions(+), 122 deletions(-) diff --git a/libs/apiPoloniex.js b/libs/apiPoloniex.js index 35f330c..fc483e1 100644 --- a/libs/apiPoloniex.js +++ b/libs/apiPoloniex.js @@ -8,7 +8,7 @@ module.exports = function() { // Constants var version = '0.1.0', - PUBLIC_API_URL = 'http://poloniex.com/public', + PUBLIC_API_URL = 'https://poloniex.com/public', PRIVATE_API_URL = 'https://poloniex.com/tradingApi', USER_AGENT = 'npm-crypto-apis/' + version diff --git a/libs/poolWorker.js b/libs/poolWorker.js index 69d130f..0035eeb 100644 --- a/libs/poolWorker.js +++ b/libs/poolWorker.js @@ -63,10 +63,10 @@ module.exports = function(logger){ var oldPool = pools[oldCoin]; var proxyPort = proxySwitch[algo].port; - if (newCoin == oldCoin) { + if (newCoin == oldCoin) { logger.debug(logSystem, logComponent, logSubCat, 'Switch message would have no effect - ignoring ' + newCoin); - break; - } + break; + } logger.debug(logSystem, logComponent, logSubCat, 'Proxy message for ' + algo + ' from ' + oldCoin + ' to ' + newCoin); @@ -91,8 +91,8 @@ module.exports = function(logger){ else { logger.debug(logSystem, logComponent, logSubCat, 'Last proxy state saved to redis for ' + algo); } - }); - }); + }); + }); } break; } @@ -230,9 +230,9 @@ module.exports = function(logger){ // Setup proxySwitch object to control proxy operations from configuration and any restored // state. Each algorithm has a listening port, current coin name, and an active pool to // which traffic is directed when activated in the config. - // - // In addition, the proxy config also takes diff and varDiff parmeters the override the - // defaults for the standard config of the coin. + // + // In addition, the proxy config also takes diff and varDiff parmeters the override the + // defaults for the standard config of the coin. // Object.keys(portalConfig.proxy).forEach(function(algorithm) { @@ -245,21 +245,21 @@ module.exports = function(logger){ }; - // Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up - // - // Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was - // routed into instead. - // - if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) { + // Copy diff and vardiff configuation into pools that match our algorithm so the stratum server can pick them up + // + // Note: This seems a bit wonky and brittle - better if proxy just used the diff config of the port it was + // routed into instead. + // + if (portalConfig.proxy[algorithm].hasOwnProperty('varDiff')) { proxySwitch[algorithm].varDiff = new Stratum.varDiff(proxySwitch[algorithm].port, portalConfig.proxy[algorithm].varDiff); proxySwitch[algorithm].diff = portalConfig.proxy[algorithm].diff; - } + } Object.keys(pools).forEach(function (coinName) { var a = poolConfigs[coinName].coin.algorithm; var p = pools[coinName]; - if (a === algorithm) { + if (a === algorithm) { p.setVarDiff(proxySwitch[algorithm].port, proxySwitch[algorithm].varDiff); - } + } }); proxySwitch[algorithm].proxy = net.createServer(function(socket) { @@ -267,12 +267,12 @@ module.exports = function(logger){ var logSubCat = 'Thread ' + (parseInt(forkId) + 1); logger.debug(logSystem, 'Connect', logSubCat, 'Proxy connect from ' + socket.remoteAddress + ' on ' + proxySwitch[algorithm].port - + ' routing to ' + currentPool); + + ' routing to ' + currentPool); pools[currentPool].getStratumServer().handleNewClient(socket); }).listen(parseInt(proxySwitch[algorithm].port), function() { logger.debug(logSystem, logComponent, logSubCat, 'Proxy listening for ' + algorithm + ' on port ' + proxySwitch[algorithm].port - + ' into ' + proxySwitch[algorithm].currentPool); + + ' into ' + proxySwitch[algorithm].currentPool); }); } else { diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 7d57fd7..3d06d77 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,5 +1,6 @@ var async = require('async'); +var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); var Stratum = require('stratum-pool'); @@ -16,7 +17,7 @@ module.exports = function(logger){ // build status tracker for collecting coin market information // var profitStatus = {}; - var profitSymbols = {}; + var symbolToAlgorithmMap = {}; Object.keys(poolConfigs).forEach(function(coin){ var poolConfig = poolConfigs[coin]; @@ -30,12 +31,10 @@ module.exports = function(logger){ symbol: poolConfig.coin.symbol, difficulty: 0, reward: 0, - prices: {}, - depths: {}, - volumes: {}, + exchangeInfo: {} }; profitStatus[algo][poolConfig.coin.symbol] = coinStatus; - profitSymbols[poolConfig.coin.symbol] = algo; + symbolToAlgorithmMap[poolConfig.coin.symbol] = algo; }); @@ -52,8 +51,6 @@ module.exports = function(logger){ 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)); // @@ -63,6 +60,10 @@ module.exports = function(logger){ // 'API_KEY', // 'API_SECRET' ); + var cryptsyApi = new Cryptsy( + // 'API_KEY', + // 'API_SECRET' + ); // // market data collection from Poloniex @@ -75,51 +76,34 @@ module.exports = function(logger){ taskCallback(err); return; } - Object.keys(profitSymbols).forEach(function(symbol){ - var btcPrice = new Number(0); - var ltcPrice = new Number(0); + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Poloniex')) + exchangeInfo['Poloniex'] = {}; + var marketData = exchangeInfo['Poloniex']; if (data.hasOwnProperty('BTC_' + symbol)) { - btcPrice = new Number(data['BTC_' + symbol]); + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + var btcData = data['BTC_' + symbol]; + marketData['BTC'].ask = new Number(btcData.lowestAsk); + marketData['BTC'].bid = new Number(btcData.highestBid); + marketData['BTC'].last = new Number(btcData.last); + marketData['BTC'].baseVolume = new Number(btcData.baseVolume); + marketData['BTC'].quoteVolume = new Number(btcData.quoteVolume); } if (data.hasOwnProperty('LTC_' + symbol)) { - ltcPrice = new Number(data['LTC_' + symbol]); - } + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - 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; + var ltcData = data['LTC_' + symbol]; + marketData['LTC'].ask = new Number(ltcData.lowestAsk); + marketData['LTC'].bid = new Number(ltcData.highestBid); + marketData['LTC'].last = new Number(ltcData.last); + marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); + marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); } }); taskCallback(); @@ -127,34 +111,25 @@ module.exports = function(logger){ }, 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) - }); - } + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Poloniex']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) + }); } }); - if (depthTasks.length == 0){ - taskCallback; + if (!depthTasks.length){ + taskCallback(); return; } - async.parallel(depthTasks, function(err){ + async.series(depthTasks, function(err){ if (err){ taskCallback(err); return; @@ -181,37 +156,110 @@ module.exports = function(logger){ if (data.hasOwnProperty('bids')){ data['bids'].forEach(function(order){ var price = new Number(order[0]); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); var qty = new Number(order[1]); // only measure the depth down to configured depth - if (price >= coinPrice * portalConfig.profitSwitch.depth){ + if (price >= limit){ 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; + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; + marketData[symbolA].depth = depth; callback(); }); }; - // TODO + this.getProfitDataCryptsy = function(callback){ - callback(null); + async.series([ + function(taskCallback){ + cryptsyApi.getTicker(function(err, data){ + if (err || data.success != 1){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Cryptsy')) + exchangeInfo['Cryptsy'] = {}; + + var marketData = exchangeInfo['Cryptsy']; + var results = data.return.markets; + + if (results.hasOwnProperty(symbol + '/BTC')) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + var btcData = results[symbol + '/BTC']; + marketData['BTC'].last = new Number(btcData.lasttradeprice); + marketData['BTC'].baseVolume = new Number(marketData['BTC'].last / btcData.volume); + marketData['BTC'].quoteVolume = new Number(btcData.volume); + if (btcData.sellorders != null) + marketData['BTC'].ask = new Number(btcData.sellorders[0].price); + if (btcData.buyorders != null) { + marketData['BTC'].bid = new Number(btcData.buyorders[0].price); + var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); + var depth = new Number(0); + btcData['buyorders'].forEach(function(order){ + var price = new Number(order.price); + var qty = new Number(order.quantity); + if (price >= limit){ + depth += (qty * price); + } + }); + marketData['BTC'].depth = depth; + } + } + + if (data.hasOwnProperty(symbol + '/LTC')) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + var ltcData = results[symbol + '/LTC']; + marketData['LTC'].last = new Number(ltcData.lasttradeprice); + marketData['LTC'].baseVolume = new Number(marketData['LTC'].last / ltcData.volume); + marketData['LTC'].quoteVolume = new Number(ltcData.volume); + if (ltcData.sellorders != null) + marketData['LTC'].ask = new Number(ltcData.sellorders[0].price); + if (ltcData.buyorders != null) { + marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); + var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); + var depth = new Number(0); + ltcData['buyorders'].forEach(function(order){ + var price = new Number(order.price); + var qty = new Number(order.quantity); + if (price >= limit){ + depth += (qty * price); + } + }); + marketData['LTC'].depth = depth; + } + } + }); + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }; + this.getCoindDaemonInfo = function(callback){ var daemonTasks = []; Object.keys(profitStatus).forEach(function(algo){ Object.keys(profitStatus[algo]).forEach(function(symbol){ var coinName = profitStatus[algo][symbol].name; var poolConfig = poolConfigs[coinName]; - var daemonConfig = poolConfig.shareProcessing.internal.daemon; + var daemonConfig = poolConfig.shareProcessing.internal.daemon; daemonTasks.push(function(callback){ _this.getDaemonInfoForCoin(symbol, daemonConfig, callback) }); @@ -222,7 +270,7 @@ module.exports = function(logger){ callback(); return; } - async.parallel(daemonTasks, function(err){ + async.series(daemonTasks, function(err){ if (err){ callback(err); return; @@ -232,26 +280,36 @@ module.exports = function(logger){ }; this.getDaemonInfoForCoin = function(symbol, cfg, callback){ var daemon = new Stratum.daemon.interface([cfg]); - daemon.once('online', function(){ - daemon.cmd('getdifficulty', null, function(result){ - if (result[0].error != null){ - callback(result[0].error); - return; - } - profitStatus[profitSymbols[symbol]][symbol].difficulty = result[0].response; - - daemon.cmd('getblocktemplate', - [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], - function(result){ - if (result[0].error != null){ - callback(result[0].error); - return; - } - profitStatus[profitSymbols[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); - }); - callback(null) - }); - }).once('connectionFailed', function(error){ + daemon.once('online', function(){ + async.parallel([ + function(taskCallback){ + daemon.cmd('getdifficulty', null, function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; + taskCallback(null); + }); + }, + function(taskCallback){ + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); + taskCallback(null); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }).once('connectionFailed', function(error){ callback(error); }).on('error', function(error){ callback(error); From 4469cf546c9643f3e0c3eb314d31416cd225f075 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 02:42:38 +0000 Subject: [PATCH 06/12] profit data working --- config_example.json | 9 ++++-- libs/apiMintpal.js | 28 +++++++++--------- libs/profitSwitch.js | 67 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/config_example.json b/config_example.json index 5a47449..d28b0aa 100644 --- a/config_example.json +++ b/config_example.json @@ -70,9 +70,12 @@ }, "profitSwitch": { - "enabled": false, - "updateInterval": 60, - "depth": 0.80 + "enabled": false, + "updateInterval": 600, + "depth": 0.90, + "usePoloniex": true, + "useCryptsy": true, + "useMintpal": true }, "redisBlockNotifyListener": { diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js index 6563ab5..bd0610c 100644 --- a/libs/apiMintpal.js +++ b/libs/apiMintpal.js @@ -8,19 +8,19 @@ module.exports = function() { // Constants var version = '0.1.0', - PUBLIC_API_URL = 'http://pubapi.cryptsy.com/api.php', - PRIVATE_API_URL = 'https://api.cryptsy.com/api', + PUBLIC_API_URL = 'https://api.mintpal.com/v2/market', + PRIVATE_API_URL = 'https://api.mintpal.com/v2/market', USER_AGENT = 'nomp/node-open-mining-portal' // Constructor - function Cryptsy(key, secret){ + function Mintpal(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 'Cryptsy: Error. API key and secret required'; + throw 'Mintpal: Error. API key and secret required'; } // Sort parameters alphabetically and convert to `arg1=foo&arg2=bar` @@ -38,7 +38,7 @@ module.exports = function() { } // If a site uses non-trusted SSL certificates, set this value to false - Cryptsy.STRICT_SSL = true; + Mintpal.STRICT_SSL = true; // Helper methods function joinCurrencies(currencyA, currencyB){ @@ -46,8 +46,8 @@ module.exports = function() { } // Prototype - Cryptsy.prototype = { - constructor: Cryptsy, + Mintpal.prototype = { + constructor: Mintpal, // Make an API request _request: function(options, callback){ @@ -57,7 +57,7 @@ module.exports = function() { options.headers['User-Agent'] = USER_AGENT; options.json = true; - options.strictSSL = Cryptsy.STRICT_SSL; + options.strictSSL = Mintpal.STRICT_SSL; request(options, function(err, response, body) { callback(err, body); @@ -99,11 +99,13 @@ module.exports = function() { // PUBLIC METHODS getTicker: function(callback){ - var parameters = { - method: 'marketdatav2' - }; + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/summary', + qs: null + }; - return this._public(parameters, callback); + return this._request(options, callback); }, getOrderBook: function(currencyA, currencyB, callback){ @@ -200,5 +202,5 @@ module.exports = function() { } }; - return Cryptsy; + return Mintpal; }(); diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 3d06d77..d8dac3d 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -2,6 +2,7 @@ var async = require('async'); var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); +var Mintpal = require('./apiMintpal.js'); var Stratum = require('stratum-pool'); module.exports = function(logger){ @@ -64,6 +65,10 @@ module.exports = function(logger){ // 'API_KEY', // 'API_SECRET' ); + var mintpalApi = new Mintpal( + // 'API_KEY', + // 'API_SECRET' + ); // // market data collection from Poloniex @@ -253,6 +258,50 @@ module.exports = function(logger){ }; + this.getProfitDataMintpal = function(callback){ + async.series([ + function(taskCallback){ + mintpalApi.getTicker(function(err, response){ + if (err){ + taskCallback(err); + return; + } + + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + response.data.forEach(function(market){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Mintpal')) + exchangeInfo['Mintpal'] = {}; + + var marketData = exchangeInfo['Mintpal']; + + if (market.exchange == 'BTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; + + marketData['BTC'].last = new Number(market.last_price); + marketData['BTC'].baseVolume = new Number(market['24hvol']); + marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['BTC'].ask = new Number(market.top_ask); + marketData['BTC'].bid = new Number(market.top_bid); + } + + }); + }); + taskCallback(); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + + }; + + this.getCoindDaemonInfo = function(callback){ var daemonTasks = []; Object.keys(profitStatus).forEach(function(algo){ @@ -320,11 +369,19 @@ module.exports = function(logger){ var checkProfitability = function(){ logger.debug(logSystem, 'Check', 'Running mining profitability check.'); - async.parallel([ - _this.getProfitDataPoloniex, - _this.getProfitDataCryptsy, - _this.getCoindDaemonInfo - ], function(err){ + profitabilityTasks = []; + if (portalConfig.profitSwitch.usePoloniex) + profitabilityTasks.push(_this.getProfitDataPoloniex); + + if (portalConfig.profitSwitch.useCryptsy) + profitabilityTasks.push(_this.getProfitDataCryptsy); + + if (portalConfig.profitSwitch.useMintpal) + profitabilityTasks.push(_this.getProfitDataMintpal); + + profitabilityTasks.push(_this.getCoindDaemonInfo); + + async.parallel(profitabilityTasks, function(err){ if (err){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; From 2e77108b3c47206f0d952e4dd7b75490836c4d2c Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 03:29:37 +0000 Subject: [PATCH 07/12] final mintpal changes --- libs/apiMintpal.js | 10 +++++++ libs/profitSwitch.js | 66 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/libs/apiMintpal.js b/libs/apiMintpal.js index bd0610c..fec22e6 100644 --- a/libs/apiMintpal.js +++ b/libs/apiMintpal.js @@ -108,6 +108,16 @@ module.exports = function() { return this._request(options, callback); }, + getBuyOrderBook: function(currencyA, currencyB, callback){ + var options = { + method: 'GET', + url: PUBLIC_API_URL + '/orders/' + currencyB + '/' + currencyA + '/BUY', + qs: null + }; + + return this._request(options, callback); + }, + getOrderBook: function(currencyA, currencyB, callback){ var parameters = { command: 'returnOrderBook', diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index d8dac3d..a17f640 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -286,10 +286,49 @@ module.exports = function(logger){ marketData['BTC'].bid = new Number(market.top_bid); } + if (market.exchange == 'LTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; + + marketData['LTC'].last = new Number(market.last_price); + marketData['LTC'].baseVolume = new Number(market['24hvol']); + marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['LTC'].ask = new Number(market.top_ask); + marketData['LTC'].bid = new Number(market.top_bid); + } + }); }); taskCallback(); }); + }, + function(taskCallback){ + var depthTasks = []; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Mintpal']; + if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) + }); + } + if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ + depthTasks.push(function(callback){ + _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) + }); + } + }); + + if (!depthTasks.length){ + taskCallback(); + return; + } + async.series(depthTasks, function(err){ + if (err){ + taskCallback(err); + return; + } + taskCallback(); + }); } ], function(err){ if (err){ @@ -298,7 +337,31 @@ module.exports = function(logger){ } callback(null); }); - + }; + this.getMarketDepthFromMintpal = function(symbolA, symbolB, coinPrice, callback){ + mintpalApi.getBuyOrderBook(symbolA, symbolB, function(err, response){ + if (err){ + callback(err); + return; + } + var depth = new Number(0); + if (response.hasOwnProperty('data')){ + response['data'].forEach(function(order){ + var price = new Number(order.price); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var qty = new Number(order.amount); + // only measure the depth down to configured depth + if (price >= limit){ + depth += (qty * price); + } + }); + } + + + var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; + marketData[symbolA].depth = depth; + callback(); + }); }; @@ -386,7 +449,6 @@ module.exports = function(logger){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; } - logger.debug(logSystem, 'Check', JSON.stringify(profitStatus)); }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From ba30133bb2529d54405e09949193a28ebae7b8b1 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 22:55:54 +0000 Subject: [PATCH 08/12] final changes --- libs/profitSwitch.js | 113 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index a17f640..8074e33 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,4 +1,5 @@ var async = require('async'); +var net = require('net'); var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); @@ -42,13 +43,16 @@ module.exports = function(logger){ // // 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 (Object.keys(profitStatus[algo]).length <= 1) { + delete profitStatus[algo]; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + if (symbolToAlgorithmMap[symbol] === algo) + delete symbolToAlgorithmMap[symbol]; + }); } }); - if (!isMoreThanOneCoin){ + if (Object.keys(profitStatus).length == 0){ logger.debug(logSystem, 'Config', 'No alternative coins to switch to in current config, switching disabled.'); return; } @@ -110,7 +114,13 @@ module.exports = function(logger){ marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); } + // save LTC to BTC exchange rate + if (marketData.hasOwnProperty('LTC') && data.hasOwnProperty('BTC_LTC')) { + var btcLtc = data['BTC_LTC']; + marketData['LTC'].ltcToBtc = new Number(btcLtc.highestBid); + } }); + taskCallback(); }); }, @@ -158,6 +168,7 @@ module.exports = function(logger){ return; } var depth = new Number(0); + var totalQty = new Number(0); if (data.hasOwnProperty('bids')){ data['bids'].forEach(function(order){ var price = new Number(order[0]); @@ -166,12 +177,15 @@ module.exports = function(logger){ // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); } var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); }; @@ -194,7 +208,7 @@ module.exports = function(logger){ var marketData = exchangeInfo['Cryptsy']; var results = data.return.markets; - if (results.hasOwnProperty(symbol + '/BTC')) { + if (results && results.hasOwnProperty(symbol + '/BTC')) { if (!marketData.hasOwnProperty('BTC')) marketData['BTC'] = {}; @@ -208,18 +222,22 @@ module.exports = function(logger){ marketData['BTC'].bid = new Number(btcData.buyorders[0].price); var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); + var totalQty = new Number(0); btcData['buyorders'].forEach(function(order){ var price = new Number(order.price); var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); marketData['BTC'].depth = depth; + if (totalQty > 0) + marketData['BTC'].weightedBid = new Number(depth / totalQty); } } - if (data.hasOwnProperty(symbol + '/LTC')) { + if (results && results.hasOwnProperty(symbol + '/LTC')) { if (!marketData.hasOwnProperty('LTC')) marketData['LTC'] = {}; @@ -233,14 +251,18 @@ module.exports = function(logger){ marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); + var totalQty = new Number(0); ltcData['buyorders'].forEach(function(order){ var price = new Number(order.price); var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); marketData['LTC'].depth = depth; + if (totalQty > 0) + marketData['LTC'].weightedBid = new Number(depth / totalQty); } } }); @@ -262,7 +284,7 @@ module.exports = function(logger){ async.series([ function(taskCallback){ mintpalApi.getTicker(function(err, response){ - if (err){ + if (err || !response.data){ taskCallback(err); return; } @@ -346,6 +368,7 @@ module.exports = function(logger){ } var depth = new Number(0); if (response.hasOwnProperty('data')){ + var totalQty = new Number(0); response['data'].forEach(function(order){ var price = new Number(order.price); var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); @@ -353,13 +376,15 @@ module.exports = function(logger){ // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); + totalQty += qty; } }); } - var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; marketData[symbolA].depth = depth; + if (totalQty > 0) + marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); }; @@ -429,8 +454,70 @@ module.exports = function(logger){ }; + this.getMiningRate = function(callback){ + var daemonTasks = []; + Object.keys(profitStatus).forEach(function(algo){ + Object.keys(profitStatus[algo]).forEach(function(symbol){ + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); + coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); + }); + }); + callback(null); + }; + + + this.switchToMostProfitableCoins = function() { + Object.keys(profitStatus).forEach(function(algo) { + var algoStatus = profitStatus[algo]; + + var bestExchange; + var bestCoin; + var bestBtcPerMhPerHour = new Number(0); + + Object.keys(profitStatus[algo]).forEach(function(symbol) { + var coinStatus = profitStatus[algo][symbol]; + + Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ + var exchangeData = coinStatus.exchangeInfo[exchange]; + if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; + logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + } + if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); + logger.debug(logSystem, 'LTC Check', 'btcPerMhPerHour = ' + btcPerMhPerHour); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; + logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + } + }); + }); + logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + if (portalConfig.coinSwitchListener.enabled){ + var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { + client.write(JSON.stringify({ + password: portalConfig.coinSwitchListener.password, + coin: bestCoin + }) + '\n'); + }); + } + }); + }; + + var checkProfitability = function(){ - logger.debug(logSystem, 'Check', 'Running mining profitability check.'); + logger.debug(logSystem, 'Check', 'Collecting profitability data.'); profitabilityTasks = []; if (portalConfig.profitSwitch.usePoloniex) @@ -443,12 +530,18 @@ module.exports = function(logger){ profitabilityTasks.push(_this.getProfitDataMintpal); profitabilityTasks.push(_this.getCoindDaemonInfo); + profitabilityTasks.push(_this.getMiningRate); - async.parallel(profitabilityTasks, function(err){ + // has to be series + async.series(profitabilityTasks, function(err){ if (err){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; } + // + // TODO offer support for a userConfigurable function for deciding on coin to override the default + // + _this.switchToMostProfitableCoins(); }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 60b73a43a8350978f096579ec4decd3bda7999f3 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 22:57:10 +0000 Subject: [PATCH 09/12] final changes --- config_example.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config_example.json b/config_example.json index d28b0aa..869b3db 100644 --- a/config_example.json +++ b/config_example.json @@ -34,6 +34,7 @@ "coinSwitchListener": { "enabled": false, + "host": "127.0.0.1", "port": 8118, "password": "test" }, From 1134f13a4051ab03239af39851d6ae951862eaa6 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Thu, 17 Apr 2014 23:09:53 +0000 Subject: [PATCH 10/12] remove extra logging --- libs/profitSwitch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 8074e33..c1a96bc 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -492,7 +492,6 @@ module.exports = function(logger){ } if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); - logger.debug(logSystem, 'LTC Check', 'btcPerMhPerHour = ' + btcPerMhPerHour); if (btcPerMhPerHour > bestBtcPerMhPerHour){ bestBtcPerMhPerHour = btcPerMhPerHour; bestExchange = exchange; From b7e8f67f701a644e88516eabf617696618c703eb Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Fri, 18 Apr 2014 12:48:59 +0000 Subject: [PATCH 11/12] removed tabs --- libs/profitSwitch.js | 328 +++++++++++++++++++++---------------------- 1 file changed, 164 insertions(+), 164 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index c1a96bc..1705daa 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -46,10 +46,10 @@ module.exports = function(logger){ Object.keys(profitStatus).forEach(function(algo){ if (Object.keys(profitStatus[algo]).length <= 1) { delete profitStatus[algo]; - Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ - if (symbolToAlgorithmMap[symbol] === algo) - delete symbolToAlgorithmMap[symbol]; - }); + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + if (symbolToAlgorithmMap[symbol] === algo) + delete symbolToAlgorithmMap[symbol]; + }); } }); if (Object.keys(profitStatus).length == 0){ @@ -88,15 +88,15 @@ module.exports = function(logger){ Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; - if (!exchangeInfo.hasOwnProperty('Poloniex')) - exchangeInfo['Poloniex'] = {}; - var marketData = exchangeInfo['Poloniex']; + if (!exchangeInfo.hasOwnProperty('Poloniex')) + exchangeInfo['Poloniex'] = {}; + var marketData = exchangeInfo['Poloniex']; if (data.hasOwnProperty('BTC_' + symbol)) { - if (!marketData.hasOwnProperty('BTC')) - marketData['BTC'] = {}; + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; - var btcData = data['BTC_' + symbol]; + var btcData = data['BTC_' + symbol]; marketData['BTC'].ask = new Number(btcData.lowestAsk); marketData['BTC'].bid = new Number(btcData.highestBid); marketData['BTC'].last = new Number(btcData.last); @@ -104,21 +104,21 @@ module.exports = function(logger){ marketData['BTC'].quoteVolume = new Number(btcData.quoteVolume); } if (data.hasOwnProperty('LTC_' + symbol)) { - if (!marketData.hasOwnProperty('LTC')) - marketData['LTC'] = {}; + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - var ltcData = data['LTC_' + symbol]; + var ltcData = data['LTC_' + symbol]; marketData['LTC'].ask = new Number(ltcData.lowestAsk); marketData['LTC'].bid = new Number(ltcData.highestBid); marketData['LTC'].last = new Number(ltcData.last); marketData['LTC'].baseVolume = new Number(ltcData.baseVolume); marketData['LTC'].quoteVolume = new Number(ltcData.quoteVolume); } - // save LTC to BTC exchange rate + // save LTC to BTC exchange rate if (marketData.hasOwnProperty('LTC') && data.hasOwnProperty('BTC_LTC')) { - var btcLtc = data['BTC_LTC']; + var btcLtc = data['BTC_LTC']; marketData['LTC'].ltcToBtc = new Number(btcLtc.highestBid); - } + } }); taskCallback(); @@ -130,13 +130,13 @@ module.exports = function(logger){ var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Poloniex']; if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) - }); + _this.getMarketDepthFromPoloniex('BTC', symbol, marketData['BTC'].bid, callback) + }); } if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) - }); + _this.getMarketDepthFromPoloniex('LTC', symbol, marketData['LTC'].bid, callback) + }); } }); @@ -172,19 +172,19 @@ module.exports = function(logger){ if (data.hasOwnProperty('bids')){ data['bids'].forEach(function(order){ var price = new Number(order[0]); - var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); var qty = new Number(order[1]); // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } }); } var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Poloniex']; marketData[symbolA].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); @@ -202,25 +202,25 @@ module.exports = function(logger){ Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; - if (!exchangeInfo.hasOwnProperty('Cryptsy')) - exchangeInfo['Cryptsy'] = {}; + if (!exchangeInfo.hasOwnProperty('Cryptsy')) + exchangeInfo['Cryptsy'] = {}; - var marketData = exchangeInfo['Cryptsy']; - var results = data.return.markets; + var marketData = exchangeInfo['Cryptsy']; + var results = data.return.markets; if (results && results.hasOwnProperty(symbol + '/BTC')) { - if (!marketData.hasOwnProperty('BTC')) - marketData['BTC'] = {}; + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; - var btcData = results[symbol + '/BTC']; + var btcData = results[symbol + '/BTC']; marketData['BTC'].last = new Number(btcData.lasttradeprice); marketData['BTC'].baseVolume = new Number(marketData['BTC'].last / btcData.volume); marketData['BTC'].quoteVolume = new Number(btcData.volume); - if (btcData.sellorders != null) + if (btcData.sellorders != null) marketData['BTC'].ask = new Number(btcData.sellorders[0].price); - if (btcData.buyorders != null) { + if (btcData.buyorders != null) { marketData['BTC'].bid = new Number(btcData.buyorders[0].price); - var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); + var limit = new Number(marketData['BTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); var totalQty = new Number(0); btcData['buyorders'].forEach(function(order){ @@ -228,28 +228,28 @@ module.exports = function(logger){ var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } - }); + }); marketData['BTC'].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData['BTC'].weightedBid = new Number(depth / totalQty); - } - } + } + } if (results && results.hasOwnProperty(symbol + '/LTC')) { - if (!marketData.hasOwnProperty('LTC')) - marketData['LTC'] = {}; + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - var ltcData = results[symbol + '/LTC']; + var ltcData = results[symbol + '/LTC']; marketData['LTC'].last = new Number(ltcData.lasttradeprice); marketData['LTC'].baseVolume = new Number(marketData['LTC'].last / ltcData.volume); marketData['LTC'].quoteVolume = new Number(ltcData.volume); - if (ltcData.sellorders != null) + if (ltcData.sellorders != null) marketData['LTC'].ask = new Number(ltcData.sellorders[0].price); - if (ltcData.buyorders != null) { + if (ltcData.buyorders != null) { marketData['LTC'].bid = new Number(ltcData.buyorders[0].price); - var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); + var limit = new Number(marketData['LTC'].bid * portalConfig.profitSwitch.depth); var depth = new Number(0); var totalQty = new Number(0); ltcData['buyorders'].forEach(function(order){ @@ -257,13 +257,13 @@ module.exports = function(logger){ var qty = new Number(order.quantity); if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } - }); + }); marketData['LTC'].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData['LTC'].weightedBid = new Number(depth / totalQty); - } + } } }); taskCallback(); @@ -289,38 +289,38 @@ module.exports = function(logger){ return; } - Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ - response.data.forEach(function(market){ - var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; - if (!exchangeInfo.hasOwnProperty('Mintpal')) - exchangeInfo['Mintpal'] = {}; + Object.keys(symbolToAlgorithmMap).forEach(function(symbol){ + response.data.forEach(function(market){ + var exchangeInfo = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo; + if (!exchangeInfo.hasOwnProperty('Mintpal')) + exchangeInfo['Mintpal'] = {}; - var marketData = exchangeInfo['Mintpal']; + var marketData = exchangeInfo['Mintpal']; - if (market.exchange == 'BTC' && market.code == symbol) { - if (!marketData.hasOwnProperty('BTC')) - marketData['BTC'] = {}; + if (market.exchange == 'BTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('BTC')) + marketData['BTC'] = {}; - marketData['BTC'].last = new Number(market.last_price); - marketData['BTC'].baseVolume = new Number(market['24hvol']); - marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); - marketData['BTC'].ask = new Number(market.top_ask); - marketData['BTC'].bid = new Number(market.top_bid); - } + marketData['BTC'].last = new Number(market.last_price); + marketData['BTC'].baseVolume = new Number(market['24hvol']); + marketData['BTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['BTC'].ask = new Number(market.top_ask); + marketData['BTC'].bid = new Number(market.top_bid); + } - if (market.exchange == 'LTC' && market.code == symbol) { - if (!marketData.hasOwnProperty('LTC')) - marketData['LTC'] = {}; + if (market.exchange == 'LTC' && market.code == symbol) { + if (!marketData.hasOwnProperty('LTC')) + marketData['LTC'] = {}; - marketData['LTC'].last = new Number(market.last_price); - marketData['LTC'].baseVolume = new Number(market['24hvol']); - marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); - marketData['LTC'].ask = new Number(market.top_ask); - marketData['LTC'].bid = new Number(market.top_bid); - } + marketData['LTC'].last = new Number(market.last_price); + marketData['LTC'].baseVolume = new Number(market['24hvol']); + marketData['LTC'].quoteVolume = new Number(market['24hvol'] / market.last_price); + marketData['LTC'].ask = new Number(market.top_ask); + marketData['LTC'].bid = new Number(market.top_bid); + } - }); - }); + }); + }); taskCallback(); }); }, @@ -330,13 +330,13 @@ module.exports = function(logger){ var marketData = profitStatus[symbolToAlgorithmMap[symbol]][symbol].exchangeInfo['Mintpal']; if (marketData.hasOwnProperty('BTC') && marketData['BTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) - }); + _this.getMarketDepthFromMintpal('BTC', symbol, marketData['BTC'].bid, callback) + }); } if (marketData.hasOwnProperty('LTC') && marketData['LTC'].bid > 0){ depthTasks.push(function(callback){ - _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) - }); + _this.getMarketDepthFromMintpal('LTC', symbol, marketData['LTC'].bid, callback) + }); } }); @@ -371,19 +371,19 @@ module.exports = function(logger){ var totalQty = new Number(0); response['data'].forEach(function(order){ var price = new Number(order.price); - var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); + var limit = new Number(coinPrice * portalConfig.profitSwitch.depth); var qty = new Number(order.amount); // only measure the depth down to configured depth if (price >= limit){ depth += (qty * price); - totalQty += qty; + totalQty += qty; } }); } var marketData = profitStatus[symbolToAlgorithmMap[symbolB]][symbolB].exchangeInfo['Mintpal']; marketData[symbolA].depth = depth; - if (totalQty > 0) + if (totalQty > 0) marketData[symbolA].weightedBid = new Number(depth / totalQty); callback(); }); @@ -417,36 +417,36 @@ module.exports = function(logger){ }; this.getDaemonInfoForCoin = function(symbol, cfg, callback){ var daemon = new Stratum.daemon.interface([cfg]); - daemon.once('online', function(){ - async.parallel([ - function(taskCallback){ - daemon.cmd('getdifficulty', null, function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; - taskCallback(null); - }); - }, - function(taskCallback){ - daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); - taskCallback(null); - }); - } - ], function(err){ - if (err){ - callback(err); - return; - } - callback(null); - }); - }).once('connectionFailed', function(error){ + daemon.once('online', function(){ + async.parallel([ + function(taskCallback){ + daemon.cmd('getdifficulty', null, function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; + taskCallback(null); + }); + }, + function(taskCallback){ + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ + if (result[0].error != null){ + taskCallback(result[0].error); + return; + } + profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); + taskCallback(null); + }); + } + ], function(err){ + if (err){ + callback(err); + return; + } + callback(null); + }); + }).once('connectionFailed', function(error){ callback(error); }).on('error', function(error){ callback(error); @@ -458,78 +458,78 @@ module.exports = function(logger){ var daemonTasks = []; Object.keys(profitStatus).forEach(function(algo){ Object.keys(profitStatus[algo]).forEach(function(symbol){ - var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; - coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); - coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); + coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); }); }); callback(null); }; - this.switchToMostProfitableCoins = function() { - Object.keys(profitStatus).forEach(function(algo) { - var algoStatus = profitStatus[algo]; + this.switchToMostProfitableCoins = function() { + Object.keys(profitStatus).forEach(function(algo) { + var algoStatus = profitStatus[algo]; var bestExchange; var bestCoin; var bestBtcPerMhPerHour = new Number(0); - Object.keys(profitStatus[algo]).forEach(function(symbol) { - var coinStatus = profitStatus[algo][symbol]; + Object.keys(profitStatus[algo]).forEach(function(symbol) { + var coinStatus = profitStatus[algo][symbol]; - Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ - var exchangeData = coinStatus.exchangeInfo[exchange]; - if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ - var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour); - if (btcPerMhPerHour > bestBtcPerMhPerHour){ - bestBtcPerMhPerHour = btcPerMhPerHour; - bestExchange = exchange; - bestCoin = profitStatus[algo][symbol].name; - } - coinStatus.btcPerMhPerHour = btcPerMhPerHour; + Object.keys(coinStatus.exchangeInfo).forEach(function(exchange){ + var exchangeData = coinStatus.exchangeInfo[exchange]; + if (exchangeData.hasOwnProperty('BTC') && exchangeData['BTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number(exchangeData['BTC'].weightedBid * coinStatus.coinsPerMhPerHour); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); - } - if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ - var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); - if (btcPerMhPerHour > bestBtcPerMhPerHour){ - bestBtcPerMhPerHour = btcPerMhPerHour; - bestExchange = exchange; - bestCoin = profitStatus[algo][symbol].name; - } - coinStatus.btcPerMhPerHour = btcPerMhPerHour; + } + if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ + var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); + if (btcPerMhPerHour > bestBtcPerMhPerHour){ + bestBtcPerMhPerHour = btcPerMhPerHour; + bestExchange = exchange; + bestCoin = profitStatus[algo][symbol].name; + } + coinStatus.btcPerMhPerHour = btcPerMhPerHour; logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); - } - }); - }); + } + }); + }); logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); - if (portalConfig.coinSwitchListener.enabled){ - var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { - client.write(JSON.stringify({ - password: portalConfig.coinSwitchListener.password, - coin: bestCoin - }) + '\n'); - }); - } - }); - }; + if (portalConfig.coinSwitchListener.enabled){ + var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { + client.write(JSON.stringify({ + password: portalConfig.coinSwitchListener.password, + coin: bestCoin + }) + '\n'); + }); + } + }); + }; var checkProfitability = function(){ logger.debug(logSystem, 'Check', 'Collecting profitability data.'); - profitabilityTasks = []; - if (portalConfig.profitSwitch.usePoloniex) - profitabilityTasks.push(_this.getProfitDataPoloniex); + profitabilityTasks = []; + if (portalConfig.profitSwitch.usePoloniex) + profitabilityTasks.push(_this.getProfitDataPoloniex); - if (portalConfig.profitSwitch.useCryptsy) - profitabilityTasks.push(_this.getProfitDataCryptsy); + if (portalConfig.profitSwitch.useCryptsy) + profitabilityTasks.push(_this.getProfitDataCryptsy); - if (portalConfig.profitSwitch.useMintpal) - profitabilityTasks.push(_this.getProfitDataMintpal); + if (portalConfig.profitSwitch.useMintpal) + profitabilityTasks.push(_this.getProfitDataMintpal); - profitabilityTasks.push(_this.getCoindDaemonInfo); - profitabilityTasks.push(_this.getMiningRate); + profitabilityTasks.push(_this.getCoindDaemonInfo); + profitabilityTasks.push(_this.getMiningRate); // has to be series async.series(profitabilityTasks, function(err){ @@ -537,10 +537,10 @@ module.exports = function(logger){ logger.error(logSystem, 'Check', 'Error while checking profitability: ' + err); return; } - // - // TODO offer support for a userConfigurable function for deciding on coin to override the default - // - _this.switchToMostProfitableCoins(); + // + // TODO offer support for a userConfigurable function for deciding on coin to override the default + // + _this.switchToMostProfitableCoins(); }); }; setInterval(checkProfitability, portalConfig.profitSwitch.updateInterval * 1000); From 31f6091a149662580c80e0b511602277b0549551 Mon Sep 17 00:00:00 2001 From: Jerry Brady Date: Sat, 19 Apr 2014 23:08:52 +0000 Subject: [PATCH 12/12] diff computed from block info --- libs/profitSwitch.js | 64 ++++++++++++++++++++------------------------ package.json | 3 ++- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/libs/profitSwitch.js b/libs/profitSwitch.js index 1705daa..7c1f449 100644 --- a/libs/profitSwitch.js +++ b/libs/profitSwitch.js @@ -1,10 +1,13 @@ -var async = require('async'); -var net = require('net'); +var async = require('async'); +var net = require('net'); +var bignum = require('bignum'); +var algos = require('stratum-pool/lib/algoProperties.js'); +var util = require('stratum-pool/lib/util.js'); -var Cryptsy = require('./apiCryptsy.js'); +var Cryptsy = require('./apiCryptsy.js'); var Poloniex = require('./apiPoloniex.js'); -var Mintpal = require('./apiMintpal.js'); -var Stratum = require('stratum-pool'); +var Mintpal = require('./apiMintpal.js'); +var Stratum = require('stratum-pool'); module.exports = function(logger){ @@ -418,38 +421,29 @@ module.exports = function(logger){ this.getDaemonInfoForCoin = function(symbol, cfg, callback){ var daemon = new Stratum.daemon.interface([cfg]); daemon.once('online', function(){ - async.parallel([ - function(taskCallback){ - daemon.cmd('getdifficulty', null, function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].difficulty = result[0].response; - taskCallback(null); - }); - }, - function(taskCallback){ - daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ - if (result[0].error != null){ - taskCallback(result[0].error); - return; - } - profitStatus[symbolToAlgorithmMap[symbol]][symbol].reward = new Number(result[0].response.coinbasevalue / 100000000); - taskCallback(null); - }); - } - ], function(err){ - if (err){ - callback(err); + daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], function(result){ + if (result[0].error != null){ + logger.error(logSystem, symbol, 'Error while reading daemon info: ' + JSON.stringify(result[0])); + callback(null); // fail gracefully for each coin return; } + var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; + var response = result[0].response; + + // some shitcoins dont provide target, only bits, so we need to deal with both + var target = response.target ? bignum(response.target, 16) : util.bignumFromBitsHex(response.bits); + coinStatus.difficulty = parseFloat((diff1.toNumber() / target.toNumber()).toFixed(9)); + logger.debug(logSystem, symbol, 'difficulty is ' + coinStatus.difficulty); + + coinStatus.reward = new Number(response.coinbasevalue / 100000000); callback(null); }); }).once('connectionFailed', function(error){ - callback(error); + logger.error(logSystem, symbol, JSON.stringify(error)); + callback(null); // fail gracefully for each coin }).on('error', function(error){ - callback(error); + logger.error(logSystem, symbol, JSON.stringify(error)); + callback(null); // fail gracefully for each coin }).init(); }; @@ -459,7 +453,7 @@ module.exports = function(logger){ Object.keys(profitStatus).forEach(function(algo){ Object.keys(profitStatus[algo]).forEach(function(symbol){ var coinStatus = profitStatus[symbolToAlgorithmMap[symbol]][symbol]; - coinStatus.blocksPerMhPerHour = new Number(3600 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); + coinStatus.blocksPerMhPerHour = new Number(86400 / ((coinStatus.difficulty * Math.pow(2,32)) / (1 * 1000 * 1000))); coinStatus.coinsPerMhPerHour = new Number(coinStatus.reward * coinStatus.blocksPerMhPerHour); }); }); @@ -488,7 +482,7 @@ module.exports = function(logger){ bestCoin = profitStatus[algo][symbol].name; } coinStatus.btcPerMhPerHour = btcPerMhPerHour; - logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + logger.debug(logSystem, 'CALC', 'BTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); } if (exchangeData.hasOwnProperty('LTC') && exchangeData['LTC'].hasOwnProperty('weightedBid')){ var btcPerMhPerHour = new Number((exchangeData['LTC'].weightedBid * coinStatus.coinsPerMhPerHour) * exchangeData['LTC'].ltcToBtc); @@ -498,11 +492,11 @@ module.exports = function(logger){ bestCoin = profitStatus[algo][symbol].name; } coinStatus.btcPerMhPerHour = btcPerMhPerHour; - logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + logger.debug(logSystem, 'CALC', 'LTC/' + symbol + ' on ' + exchange + ' with ' + coinStatus.btcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); } }); }); - logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/Mh/hr'); + logger.debug(logSystem, 'RESULT', 'Best coin for ' + algo + ' is ' + bestCoin + ' on ' + bestExchange + ' with ' + bestBtcPerMhPerHour.toFixed(8) + ' BTC/day per Mh/s'); if (portalConfig.coinSwitchListener.enabled){ var client = net.connect(portalConfig.coinSwitchListener.port, portalConfig.coinSwitchListener.host, function () { client.write(JSON.stringify({ diff --git a/package.json b/package.json index 2140f2c..dc3e7c1 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "colors": "*", "node-watch": "*", "request": "*", - "nonce": "*" + "nonce": "*", + "bignum": "*" }, "engines": { "node": ">=0.10"