From f6b0a5053776a1b067ef35fd42e658cc3d47223b Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 1 Aug 2014 00:34:27 +0200 Subject: [PATCH 01/57] v0.7 Indicator Plugins, Rewrite of Backtester Big release with changes to almost every file, a couple of bugfixes, a rewrite of the Backtester and the addition of indicator plugins making it easy for others to write extra indicators. --- README.md | 37 ++- app.js | 20 +- backtester.js | 191 +++++++------- config.sample.js | 11 +- indicators/MACD.js | 76 ++++++ indicators/PPO.js | 76 ++++++ indicators/template | 30 +++ services/api.js | 480 ++++++++++++++++++----------------- services/candleaggregator.js | 2 +- services/candlestorage.js | 446 ++++++++++++++++---------------- services/dataprocessor.js | 220 ++++++++-------- services/dataretriever.js | 28 +- services/loggingservice.js | 24 +- services/ordermonitor.js | 104 ++++---- services/pricemonitor.js | 98 +++---- services/profitreporter.js | 60 ++--- services/pushservice.js | 58 ++--- services/tools.js | 4 +- services/tradingadvisor.js | 118 ++------- services/tradingagent.js | 28 +- 20 files changed, 1133 insertions(+), 978 deletions(-) create mode 100644 indicators/MACD.js create mode 100644 indicators/PPO.js create mode 100644 indicators/template diff --git a/README.md b/README.md index 4c902a6..a089ca0 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ When running the bot initially make sure to run with real trading disabled: config.tradingEnabled = false; -Choose an exhange you want to trade on in the exchangeSettings: +Choose an exchange you want to trade on in the exchangeSettings: exchange: '', // Options: (bitstamp, kraken) @@ -71,6 +71,41 @@ Please read up on the following articles to help you choose the settings that be - [Candlesticks](http://en.wikipedia.org/wiki/Candlestick_chart) - [MACD](http://en.wikipedia.org/wiki/MACD) +As of version 0.7 BitBot now uses indicators as small plugins. You can create your own indicator by using the following template: + + var _ = require('underscore'); + var BigNumber = require('bignumber.js'); + + var indicator = function(options) { + + this.options = options; + + _.bindAll(this, 'calculate'); + + // indicatorOptions + // options: {The options required for your indicator to work} + + }; + + //-------------------------------------------------------------------------------HelperFunctions + + // Insert your helper functions here if needed + + //-------------------------------------------------------------------------------HelperFunctions + + indicator.prototype.calculate = function(cs) { + + // This function receives a candlestick from the trading advisor, this is the layout of a candlestick: + // {'period':timestamp, 'open':open price, 'high':high price, 'low':low price, 'close':close price, 'volume':volume, 'vwap':volume weighted average price} + + // Insert your calculation logic here + + }; + + module.exports = indicator; + +For examples on how to use this template, go have a look at one of the existing indicators in the indicators folder. + # Usage Execute the following command in the folder where you installed BitBot: diff --git a/app.js b/app.js index bd728d3..b7bc89a 100644 --- a/app.js +++ b/app.js @@ -18,7 +18,7 @@ var config = require('./config.js'); var retriever = new dataretriever(config.downloaderRefreshSeconds); var processor = new dataprocessor(config.candleStickSizeMinutes); var aggregator = new candleaggregator(config.candleStickSizeMinutes); -var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes); +var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, false); var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings); var pusher = new pushservice(config.pushOver); var monitor = new ordermonitor(); @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.6.0'); +console.log('Starting BitBot v0.7.0'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); @@ -83,7 +83,7 @@ advisor.on('advice', function(advice){ if(advice === 'buy') { agent.order(advice); - + } else if(advice === 'sell') { agent.order(advice); @@ -97,23 +97,19 @@ agent.on('realOrder',function(orderDetails){ if(config.pushOver.enabled) { pusher.send('BitBot - Order Placed!', 'Placed ' + orderDetails.orderType + ' order: (' + orderDetails.amount + '@' + orderDetails.price + ')', 'magic', 1); } - + monitor.add(orderDetails, config.orderKeepAliveMinutes); }); agent.on('simulatedOrder',function(orderDetails){ - + if(config.pushOver.enabled) { pusher.send('BitBot - Order Simulated!', 'Simulated ' + orderDetails.orderType + ' order: (' + orderDetails.amount + '@' + orderDetails.price + ')', 'magic', 1); } - orderDetails.order = 'Simulated'; - monitor.add(orderDetails, config.orderKeepAliveMinutes); - reporter.updateBalance(true); - }); monitor.on('filled', function(order) { @@ -149,7 +145,7 @@ pricemon.on('advice', function(advice) { if(advice === 'buy') { agent.order(advice); - + } else if(advice === 'sell') { agent.order(advice); @@ -160,7 +156,7 @@ pricemon.on('advice', function(advice) { reporter.on('update', function(update){ - + }); @@ -172,4 +168,4 @@ reporter.on('report', function(report){ }); -processor.initialize(); \ No newline at end of file +processor.initialize(); diff --git a/backtester.js b/backtester.js index 921f9df..d39c271 100644 --- a/backtester.js +++ b/backtester.js @@ -15,61 +15,118 @@ var config = require('./config.js'); //------------------------------IntializeModules var processor = new dataprocessor(config.candleStickSizeMinutes); -var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes); +var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, true); var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes); //------------------------------IntializeModules +//------------------------------IntializeVariables +var candleStickSizeMinutes = config.candleStickSizeMinutes; +var stopLossEnabled = config.stoplossSettings.enabled; +var initialBalance = config.backTesting.initialBalance; +var USDBalance = initialBalance; +var BTCBalance = 0; +var initialBalanceBTC = 0; +var totalBalanceInUSD = 0; +var totalBalanceInBTC = 0; +var profit = 0; +var profitPercentage = 0; +var transactionFee = 0; +var transactions = 0; +var slTransactions = 0; +var lastClose = 0; +var csPeriod = 0; +//------------------------------IntializeVariables + //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.6.0'); +console.log('Starting BitBot Back-Tester v0.7.0'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart +var createOrder = function(type, stopLoss) { + + var usableBalance = 0; + + if(type === 'buy' && USDBalance !== 0) { + + usableBalance = Number(BigNumber(USDBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + + BTCBalance = Number(BigNumber(BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(lastClose)).round(2))); + USDBalance = 0; + + transactions += 1; + + if(stopLoss) { + slTransactions += 1; + logger.log('Stop loss Triggered an order:'); + } + + logger.log('Placed buy order ' + BTCBalance + ' @ ' + lastClose); + + pricemon.setPosition('bought', lastClose); + + } else if(type === 'sell' && BTCBalance !== 0) { + + usableBalance = Number(BigNumber(BTCBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + + USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastClose)).round(2))); + BTCBalance = 0; + + transactions += 1; + + if(stopLoss) { + slTransactions += 1; + logger.log('Stop loss Triggered an order:'); + } + + logger.log('Placed sell order ' + usableBalance + ' @ ' + lastClose); + + pricemon.setPosition('sold', lastClose); + + } else { + + logger.debug('Wanted to place a ' + type + ' order @ ' + lastClose + ', but there are no more funds available to ' + type); + + } + +}; + processor.on('initialized', function(){ api.getBalance(function(err, result){ - config.backTesting.transactionFee = result.fee; + transactionFee = result.fee; var loopArray = storage.getAllCandlesSince(); var csArray = storage.getFinishedAggregatedCandleSticks(config.candleStickSizeMinutes); - config.backTesting.USDBalance = config.backTesting.initialBalance; - config.backTesting.BTCBalance = 0; - config.backTesting.intialBalanceBTC = Number(BigNumber(config.backTesting.USDBalance).dividedBy(BigNumber(_.first(loopArray).close)).round(2)); - - config.transactions = 0; - config.slTransactions = 0; + intialBalanceBTC = Number(BigNumber(USDBalance).dividedBy(BigNumber(_.first(loopArray).close)).round(2)); var candleStickSizeSeconds = config.candleStickSizeMinutes * 60; if(csArray.length > 0) { - var csPeriod = _.first(csArray).period + candleStickSizeSeconds; - - } else { - - var csPeriod = 0; + csPeriod = _.first(csArray).period + candleStickSizeSeconds; } _.each(loopArray, function(cs) { - config.backTesting.lastClose = cs.close; + lastClose = cs.close; - if(config.stoplossSettings.enabled) { + if(stopLossEnabled) { pricemon.check(cs.close); } if(cs.period + 60 === csPeriod) { - var cs = csArray.shift(); - if(config.stoplossSettings.enabled) { - pricemon.update(cs); + var candle = csArray.shift(); + if(stopLossEnabled) { + pricemon.update(candle); } logger.debug('Backtest: Created a new ' + config.candleStickSizeMinutes + ' minute candlestick!'); - logger.debug(JSON.stringify(cs)); - advisor.update(cs); + logger.debug(JSON.stringify(candle)); + advisor.update(candle); if(csArray.length > 0) { csPeriod = _.first(csArray).period + candleStickSizeSeconds; } else { @@ -79,22 +136,22 @@ processor.on('initialized', function(){ }); - config.backTesting.totalBalanceInUSD = Number(BigNumber(config.backTesting.USDBalance).plus(BigNumber(config.backTesting.BTCBalance).times(BigNumber(config.backTesting.lastClose))).round(2)); - config.backTesting.totalBalanceInBTC = Number(BigNumber(config.backTesting.BTCBalance).plus(BigNumber(config.backTesting.USDBalance).dividedBy(BigNumber(config.backTesting.lastClose))).round(2)); - config.backTesting.profit = Number(BigNumber(config.backTesting.totalBalanceInUSD).minus(BigNumber(config.backTesting.initialBalance)).round(2)); - config.backTesting.profitPercentage = Number(BigNumber(config.backTesting.profit).dividedBy(BigNumber(config.backTesting.initialBalance)).times(BigNumber(100)).round(2)); + totalBalanceInUSD = Number(BigNumber(USDBalance).plus(BigNumber(BTCBalance).times(BigNumber(lastClose))).round(2)); + totalBalanceInBTC = Number(BigNumber(BTCBalance).plus(BigNumber(USDBalance).dividedBy(BigNumber(lastClose))).round(2)); + profit = Number(BigNumber(totalBalanceInUSD).minus(BigNumber(initialBalance)).round(2)); + profitPercentage = Number(BigNumber(profit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); logger.log('----------Report----------'); - logger.log('Transaction Fee: ' + config.backTesting.transactionFee + '%'); - logger.log('Initial Balance: ' + config.backTesting.initialBalance); - logger.log('Initial Balance BTC: ' + config.backTesting.intialBalanceBTC); - logger.log('Final Balance: ' + config.backTesting.totalBalanceInUSD); - logger.log('Final Balance BTC: ' + config.backTesting.totalBalanceInBTC); - logger.log('Profit: ' + config.backTesting.profit + ' (' + config.backTesting.profitPercentage + '%)'); + logger.log('Transaction Fee: ' + transactionFee + '%'); + logger.log('Initial Balance: ' + initialBalance); + logger.log('Initial Balance BTC: ' + intialBalanceBTC); + logger.log('Final Balance: ' + totalBalanceInUSD); + logger.log('Final Balance BTC: ' + totalBalanceInBTC); + logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); logger.log('Open Price: ' + _.first(loopArray).open); logger.log('Close Price: ' + _.last(loopArray).close); - logger.log('Transactions: ' + config.transactions); - logger.log('Stop Loss Transactions: ' + config.slTransactions); + logger.log('Transactions: ' + transactions); + logger.log('Stop Loss Transactions: ' + slTransactions); logger.log('--------------------------'); }); @@ -103,72 +160,18 @@ processor.on('initialized', function(){ advisor.on('advice', function(advice){ - if(advice === 'buy' && config.backTesting.USDBalance !== 0) { - - var usableBalance = Number(BigNumber(config.backTesting.USDBalance).times(BigNumber(1).minus(BigNumber(config.backTesting.transactionFee).dividedBy(BigNumber(100))))); - - config.backTesting.BTCBalance = Number(BigNumber(config.backTesting.BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(config.backTesting.lastClose)).round(2))); - config.backTesting.USDBalance = 0; - - config.transactions += 1; - - logger.log('Placed buy order @ ' + config.backTesting.lastClose); - - pricemon.setPosition('bought', config.backTesting.lastClose); - - } else if(advice === 'sell' && config.backTesting.BTCBalance !== 0) { - - var usableBalance = Number(BigNumber(config.backTesting.BTCBalance).times(BigNumber(1).minus(BigNumber(config.backTesting.transactionFee).dividedBy(BigNumber(100))))); - - config.backTesting.USDBalance = Number(BigNumber(config.backTesting.USDBalance).plus(BigNumber(usableBalance).times(BigNumber(config.backTesting.lastClose)).round(2))); - config.backTesting.BTCBalance = 0; - - config.transactions += 1; - - logger.log('Placed sell order @ ' + config.backTesting.lastClose); - - pricemon.setPosition('sold', config.backTesting.lastClose); - - } else if(advice === 'buy' || advice === 'sell') { - - logger.log('Wanted to place a ' + advice + ' order @ ' + config.backTesting.lastClose + ', but there are no more funds available to ' + advice); - - } + if(advice !== 'hold') { + createOrder(advice); + } }); pricemon.on('advice', function(advice) { - if(advice === 'buy' && config.backTesting.USDBalance !== 0) { - - var usableBalance = Number(BigNumber(config.backTesting.USDBalance).times(BigNumber(1).minus(BigNumber(config.backTesting.transactionFee).dividedBy(BigNumber(100))))); - - config.backTesting.BTCBalance = Number(BigNumber(config.backTesting.BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(config.backTesting.lastClose)).round(2))); - config.backTesting.USDBalance = 0; - - config.transactions += 1; - config.slTransactions += 1; - - logger.log('Stop Loss Placed buy order @ ' + config.backTesting.lastClose); - - pricemon.setPosition('bought', config.backTesting.lastClose); - - } else if(advice === 'sell' && config.backTesting.BTCBalance !== 0) { - - var usableBalance = Number(BigNumber(config.backTesting.BTCBalance).times(BigNumber(1).minus(BigNumber(config.backTesting.transactionFee).dividedBy(BigNumber(100))))); - - config.backTesting.USDBalance = Number(BigNumber(config.backTesting.USDBalance).plus(BigNumber(usableBalance).times(BigNumber(config.backTesting.lastClose)).round(2))); - config.backTesting.BTCBalance = 0; - - config.transactions += 1; - config.slTransactions += 1; - - logger.log('Stop Loss Placed sell order @ ' + config.backTesting.lastClose); - - pricemon.setPosition('sold', config.backTesting.lastClose); - - } + if(advice !== 'hold') { + createOrder(advice, true); + } }); -processor.initialize(); \ No newline at end of file +processor.initialize(); diff --git a/config.sample.js b/config.sample.js index fa3188b..242af43 100644 --- a/config.sample.js +++ b/config.sample.js @@ -51,10 +51,9 @@ config.orderKeepAliveMinutes = config.candleStickSizeMinutes / 10; //------------------------------IndicatorSettings config.indicatorSettings = { indicator: 'MACD', - // Options: (MACD, PPO) - options: {neededPeriods: 26, longPeriods: 26, shortPeriods: 12, emaPeriods: 9}, - buyTreshold: 0, - sellTreshold: 0 + // Choises: Any indicator from the indicators folder + options: {neededPeriods: 26, longPeriods: 26, shortPeriods: 12, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0} + // Options needed for your indicator (Look them up in the indicator's file) }; //------------------------------IndicatorSettings @@ -63,7 +62,7 @@ config.stoplossSettings = { enabled: false, percentageBought: 1, percentageSold: 1 -} +}; //------------------------------stopLossSettings //------------------------------PushOver @@ -87,4 +86,4 @@ config.debug = true; //------------------------------UserParams -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/indicators/MACD.js b/indicators/MACD.js new file mode 100644 index 0000000..2a14cc7 --- /dev/null +++ b/indicators/MACD.js @@ -0,0 +1,76 @@ +var _ = require('underscore'); +var BigNumber = require('bignumber.js'); + +var indicator = function(options) { + + this.options = options; + this.indicator = {}; + this.previousIndicator = {}; + this.advice = 'hold'; + this.length = 0; + + _.bindAll(this, 'calculate'); + + // indicatorOptions + // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} + +}; + +//-------------------------------------------------------------------------------HelperFunctions +var calculateEma = function(periods, priceToday, previousEma) { + + if(!previousEma) { + previousEma = priceToday; + } + + var k = BigNumber(2).dividedBy(BigNumber(periods+1)); + var ema = (BigNumber(priceToday).times(k)).plus(BigNumber(previousEma).times(BigNumber(1).minus(k))); + + return BigNumber(ema).round(8); + +}; +//-------------------------------------------------------------------------------HelperFunctions + +indicator.prototype.calculate = function(cs) { + + this.length += 1; + this.previousIndicator = this.indicator; + + var usePrice = cs.close; + + var emaLong = Number(calculateEma(this.options.longPeriods, usePrice, this.previousIndicator.emaLong)); + var emaShort = Number(calculateEma(this.options.shortPeriods, usePrice, this.previousIndicator.emaShort)); + + var macd = Number(BigNumber(emaShort).minus(BigNumber(emaLong))); + var macdSignal = Number(calculateEma(this.options.emaPeriods, macd, this.previousIndicator.macdSignal)); + var macdHistogram = Number(BigNumber(macd).minus(BigNumber(macdSignal)).round(2)); + + this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'macd': macd, 'macdSignal': macdSignal, 'result': macdHistogram}; + + if(this.previousIndicator.result <= this.options.buyTreshold && this.indicator.result > this.options.buyTreshold) { + + this.advice = 'buy'; + + } else if(this.previousIndicator.result >= this.options.sellTreshold && this.indicator.result < this.options.sellTreshold) { + + this.advice = 'sell'; + + } else { + + this.advice = 'hold'; + + } + + if(this.length >= this.options.neededPeriods) { + + return this.advice; + + } else { + + return 'hold'; + + } + +}; + +module.exports = indicator; diff --git a/indicators/PPO.js b/indicators/PPO.js new file mode 100644 index 0000000..494644f --- /dev/null +++ b/indicators/PPO.js @@ -0,0 +1,76 @@ +var _ = require('underscore'); +var BigNumber = require('bignumber.js'); + +var indicator = function(options) { + + this.options = options; + this.indicator = {}; + this.previousIndicator = {}; + this.advice = 'hold'; + this.length = 0; + + _.bindAll(this, 'calculate'); + + // indicatorOptions + // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} + +}; + +//-------------------------------------------------------------------------------HelperFunctions +var calculateEma = function(periods, priceToday, previousEma) { + + if(!previousEma) { + previousEma = priceToday; + } + + var k = BigNumber(2).dividedBy(BigNumber(periods+1)); + var ema = (BigNumber(priceToday).times(k)).plus(BigNumber(previousEma).times(BigNumber(1).minus(k))); + + return BigNumber(ema).round(8); + +}; +//-------------------------------------------------------------------------------HelperFunctions + +indicator.prototype.calculate = function(cs) { + + this.length += 1; + this.previousIndicator = this.indicator; + + var usePrice = cs.close; + + var emaLong = Number(calculateEma(this.options.longPeriods, usePrice, this.previousIndicator.emaLong)); + var emaShort = Number(calculateEma(this.options.shortPeriods, usePrice, this.previousIndicator.emaShort)); + + var PPO = Number(BigNumber(emaShort).minus(BigNumber(emaLong)).dividedBy(BigNumber(emaLong)).times(BigNumber(100)).round(8)); + var PPOSignal = Number(calculateEma(this.options.emaPeriods, PPO, this.previousIndicator.PPOSignal)); + var PPOHistogram = Number(BigNumber(PPO).minus(BigNumber(PPOSignal)).round(2)); + + this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'PPO': PPO, 'PPOSignal': PPOSignal, 'result': PPOHistogram}; + + if(this.previousIndicator.result <= this.options.buyTreshold && this.indicator.result > this.options.buyTreshold) { + + this.advice = 'buy'; + + } else if(this.previousIndicator.result >= this.options.sellTreshold && this.indicator.result < this.options.sellTreshold) { + + this.advice = 'sell'; + + } else { + + this.advice = 'hold'; + + } + + if(this.length >= this.options.neededPeriods) { + + return this.advice; + + } else { + + return 'hold'; + + } + +}; + +module.exports = indicator; diff --git a/indicators/template b/indicators/template new file mode 100644 index 0000000..13dbc24 --- /dev/null +++ b/indicators/template @@ -0,0 +1,30 @@ +var _ = require('underscore'); +var BigNumber = require('bignumber.js'); + +var indicator = function(options) { + + this.options = options; + + _.bindAll(this, 'calculate'); + + // indicatorOptions + // options: {The options required for your indicator to work} + +}; + +//-------------------------------------------------------------------------------HelperFunctions + + // Insert your helper functions here if needed + +//-------------------------------------------------------------------------------HelperFunctions + +indicator.prototype.calculate = function(cs) { + + // This function receives a candlestick from the trading advisor, this is the layout of a candlestick: + // {'period':timestamp, 'open':open price, 'high':high price, 'low':low price, 'close':close price, 'volume':volume, 'vwap':volume weighted average price} + + // Insert your calculation logic here + +}; + +module.exports = indicator; diff --git a/services/api.js b/services/api.js index 4206aa8..4d8a777 100644 --- a/services/api.js +++ b/services/api.js @@ -10,452 +10,462 @@ var config = require('../config.js'); var api = function() { - this.exchange = config.exchangeSettings.exchange; - this.currencyPair = config.exchangeSettings.currencyPair; + this.exchange = config.exchangeSettings.exchange; + this.currencyPair = config.exchangeSettings.currencyPair; - if(this.exchange === 'bitstamp') { + if(this.exchange === 'bitstamp') { - var key = config.apiSettings.bitstamp.apiKey; - var secret = config.apiSettings.bitstamp.secret; - var client_id = config.apiSettings.bitstamp.clientId; - - this.bitstamp = new Bitstamp(key, secret, client_id); + var key = config.apiSettings.bitstamp.apiKey; + var secret = config.apiSettings.bitstamp.secret; + var client_id = config.apiSettings.bitstamp.clientId; - } else if(this.exchange === 'kraken') { + this.bitstamp = new Bitstamp(key, secret, client_id); - this.kraken = new Kraken(config.apiSettings.kraken.apiKey, config.apiSettings.kraken.secret); + } else if(this.exchange === 'kraken') { - } else { + this.kraken = new Kraken(config.apiSettings.kraken.apiKey, config.apiSettings.kraken.secret); - logger.error('Invalid exchange, exiting!'); - return process.exit(); + } else { - } + logger.error('Invalid exchange, exiting!'); + return process.exit(); + + } - this.q = async.queue(function (task, callback) { - task(); - setTimeout(callback,1000); - }, 1); + this.q = async.queue(function (task, callback) { + task(); + setTimeout(callback,1000); + }, 1); - _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); }; api.prototype.retry = function(method, args) { - var self = this; + var self = this; - // make sure the callback (and any other fn) - // is bound to api - _.each(args, function(arg, i) { - if(_.isFunction(arg)) - args[i] = _.bind(arg, self); - }); + // make sure the callback (and any other fn) + // is bound to api + _.each(args, function(arg, i) { + if(_.isFunction(arg)) + args[i] = _.bind(arg, self); + }); - // run the failed method again with the same - // arguments after wait - setTimeout( - function() { method.apply(self, args) }, - 1000*15); + // run the failed method again with the same + // arguments after wait + setTimeout(function() { method.apply(self, args) }, 1000*15); }; api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, cb) { - var args = _.toArray(receivedArgs); + var args = _.toArray(receivedArgs); - return function(err, result) { + return function(err, result) { - if(err) { + if(err) { - if(this.exchange === 'kraken' && err[0] === 'EQuery:Unknown asset pair') { + if(this.exchange === 'kraken' && err[0] === 'EQuery:Unknown asset pair') { - logger.error('Kraken returned Unknown asset pair error, exiting!'); - return process.exit(); + logger.error('Kraken returned Unknown asset pair error, exiting!'); + return process.exit(); - } else if(retryAllowed) { + } else if(retryAllowed) { - logger.error('Couldn\'t connect to the API, retrying in 15 seconds!'); - logger.error(JSON.stringify(err).substring(0,99)); - return this.retry(method, args); + logger.error('Couldn\'t connect to the API, retrying in 15 seconds!'); + logger.error(JSON.stringify(err).substring(0,99)); + return this.retry(method, args); - } else { + } else { - logger.error('Couldn\'t connect to the API.'); - return logger.error(JSON.stringify(err).substring(0,99)); - cb(err, result); + logger.error('Couldn\'t connect to the API.'); + cb(err, result); + return logger.error(JSON.stringify(err).substring(0,99)); - } + } - } + } - if(this.exchange === 'bitstamp' && result.error === 'Invalid nonce') { - logger.error('Bitstamp returned invalid nonce error, retrying in 15 seconds!'); - return this.retry(method, args); - } + if(this.exchange === 'bitstamp' && result.error === 'Invalid nonce') { + logger.error('Bitstamp returned invalid nonce error, retrying in 15 seconds!'); + return this.retry(method, args); + } - logger.debug('API Call Result (Substring)!'); - logger.debug(JSON.stringify(result).substring(0,99)); + logger.debug('API Call Result (Substring)!'); + logger.debug(JSON.stringify(result).substring(0,99)); - //_.last(args)(null, result); - cb(null, result); + //_.last(args)(null, result); + cb(null, result); - }.bind(this); + }.bind(this); }; api.prototype.getTrades = function(cb) { - var args = arguments; + var args = arguments; - var wrapper = function() { + var wrapper = function() { - var pair = this.currencyPair.pair; + var handler; - if(this.exchange === 'bitstamp') { + var pair = this.currencyPair.pair; - var handler = function(err, response) { + if(this.exchange === 'bitstamp') { - var trades = _.map(response, function(t) { + handler = function(err, response) { - return {date: parseInt(t.date), price: parseFloat(t.price), amount: parseFloat(t.amount)}; + var trades = _.map(response, function(t) { - }); + return {date: parseInt(t.date), price: parseFloat(t.price), amount: parseFloat(t.amount)}; - var result = _.sortBy(trades, function(trade){ return trade.date; }); + }); - cb(null, result); + var result = _.sortBy(trades, function(trade){ return trade.date; }); - } + cb(null, result); - this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, false, handler)); + }; - } else if(this.exchange === 'kraken') { + this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, false, handler)); - var handler = function(err, data) { + } else if(this.exchange === 'kraken') { - var values = _.find(data.result, function(value, key) { + handler = function(err, data) { - return key === pair; + var values = _.find(data.result, function(value, key) { - }); + return key === pair; - var trades = _.map(values, function(t) { + }); - return {date: parseInt(t[2]), price: parseFloat(t[0]), amount: parseFloat(t[1])}; + var trades = _.map(values, function(t) { - }); + return {date: parseInt(t[2]), price: parseFloat(t[0]), amount: parseFloat(t[1])}; - cb(null, trades); + }); - } + cb(null, trades); - this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, false, handler)); + }; - } + this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, false, handler)); + + } - }; + }; - this.q.push(_.bind(wrapper,this)); + this.q.push(_.bind(wrapper,this)); }; api.prototype.getBalance = function(cb) { - var args = arguments; + var args = arguments; - var wrapper = function() { + var wrapper = function() { - var asset = this.currencyPair.asset; - var currency = this.currencyPair.currency; + var handler; - var pair = this.currencyPair.pair; + var asset = this.currencyPair.asset; + var currency = this.currencyPair.currency; - if(this.exchange === 'bitstamp') { + var pair = this.currencyPair.pair; - var handler = function(err, result) { + if(this.exchange === 'bitstamp') { - cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); + handler = function(err, result) { - } + cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); - this.bitstamp.balance(this.errorHandler(this.getBalance, args, true, handler)); + }; - } else if(this.exchange === 'kraken') { + this.bitstamp.balance(this.errorHandler(this.getBalance, args, true, handler)); - var handler = function(err, data) { + } else if(this.exchange === 'kraken') { - var assetValue = _.find(data.result, function(value, key) { - return key === asset; - }); + handler = function(err, data) { - var currencyValue = _.find(data.result, function(value, key) { - return key === currency; - }); + var assetValue = _.find(data.result, function(value, key) { + return key === asset; + }); - if(!assetValue) { - assetValue = 0; - } + var currencyValue = _.find(data.result, function(value, key) { + return key === currency; + }); - if(!currencyValue) { - currencyValue = 0; - } + if(!assetValue) { + assetValue = 0; + } - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, true, function(err, data) { + if(!currencyValue) { + currencyValue = 0; + } - var fee = parseFloat(_.find(data.result.fees, function(value, key) { - return key === pair; - }).fee); + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, true, function(err, data) { - cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + var fee = parseFloat(_.find(data.result.fees, function(value, key) { + return key === pair; + }).fee); - })); + cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); - }.bind(this); + })); - this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, true, handler)); + }.bind(this); - } + this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, true, handler)); - }; + } - this.q.push(_.bind(wrapper,this)); + }; + + this.q.push(_.bind(wrapper,this)); }; api.prototype.getOrderBook = function(cb) { - var args = arguments; + var args = arguments; - var wrapper = function () { + var wrapper = function () { - var pair = this.currencyPair.pair; + var handler; - if(this.exchange === 'bitstamp') { + var pair = this.currencyPair.pair; - var handler = function(err, result) { + if(this.exchange === 'bitstamp') { - var bids = _.map(result.bids, function(bid) { - return {assetAmount: bid[1], currencyPrice: bid[0]}; - }); + handler = function(err, result) { - var asks = _.map(result.asks, function(ask) { - return {assetAmount: ask[1], currencyPrice: ask[0]}; - }); + var bids = _.map(result.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); - cb(null, {bids: bids, asks: asks}); + var asks = _.map(result.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); - }; + cb(null, {bids: bids, asks: asks}); - this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, true, handler)); + }; - } else if(this.exchange === 'kraken') { + this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, true, handler)); - var handler = function(err, data) { + } else if(this.exchange === 'kraken') { - var orderbook = _.find(data.result, function(value, key) { + handler = function(err, data) { - return key === pair; + var orderbook = _.find(data.result, function(value, key) { - }); + return key === pair; - var bids = _.map(orderbook.bids, function(bid) { - return {assetAmount: bid[1], currencyPrice: bid[0]}; - }); + }); - var asks = _.map(orderbook.asks, function(ask) { - return {assetAmount: ask[1], currencyPrice: ask[0]}; - }); + var bids = _.map(orderbook.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); - cb(null, {bids: bids, asks: asks}); + var asks = _.map(orderbook.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); - }; + cb(null, {bids: bids, asks: asks}); + }; - this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, true, handler)); + this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, true, handler)); - } + } - }; + }; - this.q.push(_.bind(wrapper,this)); + this.q.push(_.bind(wrapper,this)); }; api.prototype.placeOrder = function(type, amount, price, cb) { - var args = arguments; + var args = arguments; - var wrapper = function() { + var wrapper = function() { - var pair = this.currencyPair.pair; + var handler; - if(this.exchange === 'bitstamp') { + var pair = this.currencyPair.pair; - var handler = function(err, result) { + if(this.exchange === 'bitstamp') { - cb(null, {txid: result.id}); + handler = function(err, result) { - }; + cb(null, {txid: result.id}); - if(type === 'buy') { + }; - this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, true, handler)); + if(type === 'buy') { - } else if (type === 'sell') { + this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, true, handler)); - this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, true, handler)); + } else if (type === 'sell') { - } else { + this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, true, handler)); - logger.log('Invalid order type!'); - } + } else { - } else if(this.exchange === 'kraken') { + logger.log('Invalid order type!'); + } - var handler = function(err, data) { + } else if(this.exchange === 'kraken') { - cb(null, {txid: data.result.txid[0]}); + handler = function(err, data) { - } + cb(null, {txid: data.result.txid[0]}); - if(type === 'buy') { + }; - this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, handler)); + if(type === 'buy') { - } else if (type === 'sell') { + this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, handler)); - this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, handler)); + } else if (type === 'sell') { - } else { + this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, handler)); - logger.log('Invalid order type!'); - } + } else { - } + logger.log('Invalid order type!'); - }; + } + + } - this.q.push(_.bind(wrapper,this)); + }; + + this.q.push(_.bind(wrapper,this)); }; api.prototype.orderFilled = function(order, cb) { - var args = arguments; + var args = arguments; - var wrapper = function() { + var wrapper = function() { - if(this.exchange === 'bitstamp') { + var handler; - var handler = function(err, result) { + if(this.exchange === 'bitstamp') { - var open = _.find(result, function(o) { + handler = function(err, result) { - return o.id === order; + var open = _.find(result, function(o) { - }, this); + return o.id === order; - if(open) { + }, this); - cb(null, false); + if(open) { - } else { + cb(null, false); - cb(null, true); + } else { - } + cb(null, true); - }; + } - this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, false, handler)); + }; - } else if(this.exchange === 'kraken') { + this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, false, handler)); - var handler = function(err, data) { + } else if(this.exchange === 'kraken') { - var open = _.find(data.result.open, function(value, key) { + handler = function(err, data) { - return key === order; + var open = _.find(data.result.open, function(value, key) { - }); + return key === order; - if(open) { + }); - cb(null, false); + if(open) { - } else { + cb(null, false); - cb(null, true); + } else { - } + cb(null, true); - }; + } - this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, false, handler)); + }; - } + this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, false, handler)); - }; + } - this.q.push(_.bind(wrapper,this)); + }; + + this.q.push(_.bind(wrapper,this)); }; api.prototype.cancelOrder = function(order, cb) { - var args = arguments; - - var wrapper = function() { + var args = arguments; - if(this.exchange === 'bitstamp') { + var wrapper = function() { - var handler = function(err, result) { + var handler; - if(!result.error) { - cb(null, true); - } else { - cb(null, false); - } + if(this.exchange === 'bitstamp') { - }; + handler = function(err, result) { - this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, true, handler)); + if(!result.error) { + cb(null, true); + } else { + cb(null, false); + } - } else if(this.exchange === 'kraken') { + }; - this.orderFilled(order, function(err, filled) { + this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, true, handler)); - if(!filled) { + } else if(this.exchange === 'kraken') { - var handler = function(err, result) { + this.orderFilled(order, function(err, filled) { - if(result.count > 0) { - cb(null, true); - } else { - cb(null, false); - } + if(!filled) { - }; + handler = function(err, result) { - this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, true, handler)); + if(result.count > 0) { + cb(null, true); + } else { + cb(null, false); + } - } else { + }; - cb(null, false); + this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, true, handler)); - } + } else { - }.bind(this)); + cb(null, false); } - }; + }.bind(this)); + + } + + }; - this.q.push(_.bind(wrapper,this)); + this.q.push(_.bind(wrapper,this)); }; var apiservice = new api(); -module.exports = apiservice; \ No newline at end of file +module.exports = apiservice; diff --git a/services/candleaggregator.js b/services/candleaggregator.js index aee5673..f25c736 100644 --- a/services/candleaggregator.js +++ b/services/candleaggregator.js @@ -46,4 +46,4 @@ aggregator.prototype.update = function() { }; -module.exports = aggregator; \ No newline at end of file +module.exports = aggregator; diff --git a/services/candlestorage.js b/services/candlestorage.js index 436b3c2..0c69e0f 100644 --- a/services/candlestorage.js +++ b/services/candlestorage.js @@ -12,455 +12,455 @@ var storage = function() { this.dbCollectionName = config.exchangeSettings.exchange + config.exchangeSettings.currencyPair.pair; - this.candleSticksCollection = []; + this.candleSticksCollection = []; - _.bindAll(this, 'selectCollection', 'set', 'push', 'removeOldCandles', 'flush', 'getAllCandlesSince', 'getLastNCandles', 'getLastPeriod', 'getLastNonEmptyPeriod', 'getLastClose', 'getLastNonEmptyClose', 'getCandle', 'length', 'getAverageCandleStickSize', 'generateWebServerArray', 'getFinishedAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getAggregatedCandleSticks', 'materialise', 'removeOldDBCandles', 'getDBCandles'); + _.bindAll(this, 'selectCollection', 'set', 'push', 'removeOldCandles', 'flush', 'getAllCandlesSince', 'getLastNCandles', 'getLastPeriod', 'getLastNonEmptyPeriod', 'getLastClose', 'getLastNonEmptyClose', 'getCandle', 'length', 'getAverageCandleStickSize', 'generateWebServerArray', 'getFinishedAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getAggregatedCandleSticks', 'materialise', 'removeOldDBCandles', 'getDBCandles'); }; storage.prototype.selectCollection = function(candleStickSize) { - var selectedSize = candleStickSize; + var selectedSize = candleStickSize; - if(!candleStickSize) { - selectedSize = 1; - } + if(!candleStickSize) { + selectedSize = 1; + } - var candleStickArray = _.find(this.candleSticksCollection, function(entry) { - return entry.type === selectedSize; - }); + var candleStickArray = _.find(this.candleSticksCollection, function(entry) { + return entry.type === selectedSize; + }); - if(!candleStickArray) { + if(!candleStickArray) { - this.candleSticksCollection.push({'type': selectedSize, 'candleSticks': []}); - candleStickArray = _.find(this.candleSticksCollection, function(entry) { - return entry.type === selectedSize; - }); - } + this.candleSticksCollection.push({'type': selectedSize, 'candleSticks': []}); + candleStickArray = _.find(this.candleSticksCollection, function(entry) { + return entry.type === selectedSize; + }); + } - return candleStickArray; + return candleStickArray; }; storage.prototype.set = function(candleSticks, candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - candleStickArray.candleSticks = []; + candleStickArray.candleSticks = []; - candleSticks.forEach(function(candleStick){ - candleStickArray.candleSticks.push(candleStick); - }); + candleSticks.forEach(function(candleStick){ + candleStickArray.candleSticks.push(candleStick); + }); }; storage.prototype.push = function(candleStick, candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var updated = false; + var updated = false; - candleStickArray.candleSticks = _.map(candleStickArray.candleSticks, function(entry) { + candleStickArray.candleSticks = _.map(candleStickArray.candleSticks, function(entry) { - if(entry.period === candleStick.period){ - updated = true; - return candleStick; - } else { - return entry; - } + if(entry.period === candleStick.period){ + updated = true; + return candleStick; + } else { + return entry; + } - }, this); + }, this); - if(!updated){ + if(!updated){ - candleStickArray.candleSticks.push(candleStick); + candleStickArray.candleSticks.push(candleStick); - } + } }; storage.prototype.removeOldCandles = function() { - var maxCandleSize = _.max(this.candleSticksCollection, function(collection) { - return collection.type; - }).type; + var maxCandleSize = _.max(this.candleSticksCollection, function(collection) { + return collection.type; + }).type; - var maxCandleSizeSeconds = maxCandleSize * 60; + var maxCandleSizeSeconds = maxCandleSize * 60; - _.each(this.candleSticksCollection, function(collection) { + _.each(this.candleSticksCollection, function(collection) { - var candleStickSize = collection.type; + var candleStickSize = collection.type; - var candleStickSizeSeconds = candleStickSize * 60; + var candleStickSizeSeconds = candleStickSize * 60; - var now = Math.floor(tools.unixTimeStamp(new Date().getTime()) / candleStickSizeSeconds) * candleStickSizeSeconds; - var oldPeriod = now - (maxCandleSizeSeconds * 2000); + var now = Math.floor(tools.unixTimeStamp(new Date().getTime()) / candleStickSizeSeconds) * candleStickSizeSeconds; + var oldPeriod = now - (maxCandleSizeSeconds * 2000); - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - candleStickArray.candleSticks = _.filter(candleStickArray.candleSticks, function(candleStick){ - return candleStick.period > oldPeriod; - }); + candleStickArray.candleSticks = _.filter(candleStickArray.candleSticks, function(candleStick){ + return candleStick.period > oldPeriod; + }); - if(candleStickSize === 1) { - this.removeOldDBCandles(oldPeriod); - } + if(candleStickSize === 1) { + this.removeOldDBCandles(oldPeriod); + } - }, this); + }, this); }; storage.prototype.flush = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - candleStickArray.candleSticks = []; + candleStickArray.candleSticks = []; }; storage.prototype.getAllCandlesSince = function(period, candleStickSize) { - var filterPeriod = period; + var filterPeriod = period; - if(!period) { - filterPeriod = 0; - } + if(!period) { + filterPeriod = 0; + } - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var array = _.filter(candleStickArray.candleSticks, function(candleStick) { + var array = _.filter(candleStickArray.candleSticks, function(candleStick) { - return candleStick.period >= filterPeriod; + return candleStick.period >= filterPeriod; - }); + }); - return array; + return array; }; storage.prototype.getLastNCandles = function(amount, candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var N = amount; + var N = amount; - if(!amount) { + if(!amount) { - N = 1; + N = 1; - } + } - return _.last(candleStickArray.candleSticks,N); + return _.last(candleStickArray.candleSticks,N); }; storage.prototype.getLastPeriod = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var dsLength = candleStickArray.candleSticks.length; + var dsLength = candleStickArray.candleSticks.length; - if(dsLength === 0) { - return 0; - } else { - return _.last(candleStickArray.candleSticks).period; - } + if(dsLength === 0) { + return 0; + } else { + return _.last(candleStickArray.candleSticks).period; + } }; storage.prototype.getLastNonEmptyPeriod = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var dsLength = candleStickArray.candleSticks.length; + var dsLength = candleStickArray.candleSticks.length; - if(dsLength === 0) { - return 0; - } else { - var array = _.filter(candleStickArray.candleSticks, function(candleStick) { - return candleStick.volume > 0; - }); - if(array.length === 0) { - return 0; - } else { - return _.last(array).period; - } - } + if(dsLength === 0) { + return 0; + } else { + var array = _.filter(candleStickArray.candleSticks, function(candleStick) { + return candleStick.volume > 0; + }); + if(array.length === 0) { + return 0; + } else { + return _.last(array).period; + } + } }; storage.prototype.getLastClose = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var dsLength = candleStickArray.candleSticks.length; + var dsLength = candleStickArray.candleSticks.length; - if(dsLength === 0) { - return 0; - } else { - return _.last(candleStickArray.candleSticks).close; - } + if(dsLength === 0) { + return 0; + } else { + return _.last(candleStickArray.candleSticks).close; + } }; storage.prototype.getLastNonEmptyClose = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var dsLength = candleStickArray.candleSticks.length; + var dsLength = candleStickArray.candleSticks.length; - if(dsLength === 0) { - return; - } else { - var array = _.filter(candleStickArray.candleSticks, function(candleStick) { - return candleStick.volume > 0; - }); - if(array.length === 0) { - return 0; - } else { - return _.last(array).close; - } - } + if(dsLength === 0) { + return; + } else { + var array = _.filter(candleStickArray.candleSticks, function(candleStick) { + return candleStick.volume > 0; + }); + if(array.length === 0) { + return 0; + } else { + return _.last(array).close; + } + } }; storage.prototype.getCandle = function(period, candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var result = _.find(candleStickArray.candleSticks, function(candleStick) { - return candleStick.period === period; - }); + var result = _.find(candleStickArray.candleSticks, function(candleStick) { + return candleStick.period === period; + }); - if(!result) { - return {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - } else { - return result; - } + if(!result) { + return {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + } else { + return result; + } }; storage.prototype.length = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - return candleStickArray.candleSticks.length; + return candleStickArray.candleSticks.length; }; storage.prototype.getAverageCandleStickSize = function(csamount, candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var array = _.first(_.last(candleStickArray.candleSticks, csamount + 1), csamount); + var array = _.first(_.last(candleStickArray.candleSticks, csamount + 1), csamount); - var average = 0; + var average = 0; - if(array.length > 0) { - average = Number(BigNumber(_.reduce(array, function(memo, entry){ return Number(BigNumber(memo).plus(Math.abs(Number(BigNumber(entry.close).minus(entry.open))))); }, 0)).dividedBy(BigNumber(csamount)).round(2)); - } + if(array.length > 0) { + average = Number(BigNumber(_.reduce(array, function(memo, entry){ return Number(BigNumber(memo).plus(Math.abs(Number(BigNumber(entry.close).minus(entry.open))))); }, 0)).dividedBy(BigNumber(csamount)).round(2)); + } - return average; + return average; }; storage.prototype.generateWebServerArray = function(period, candleStickSize) { - var array = this.getAllCandlesSince(period, candleStickSize); + var array = this.getAllCandlesSince(period, candleStickSize); - var result = []; + var result = []; - _.each(array,function(entry){ + _.each(array,function(entry){ - result.push([entry.period * 1000, entry.open, entry.high, entry.low, entry.close, entry.volume]); + result.push([entry.period * 1000, entry.open, entry.high, entry.low, entry.close, entry.volume]); - }); + }); - return result; + return result; }; storage.prototype.getFinishedAggregatedCandleSticks = function(candleStickSize) { - var array = this.getAggregatedCandleSticks(candleStickSize); - array = _.filter(array, function(entry){ return entry.period !== _.last(array).period; }); + var array = this.getAggregatedCandleSticks(candleStickSize); + array = _.filter(array, function(entry){ return entry.period !== _.last(array).period; }); - return array; + return array; }; storage.prototype.getLastCompleteAggregatedCandleStick = function(candleStickSize) { - var array = this.getAggregatedCandleSticks(candleStickSize); - array = _.filter(array, function(entry){ return entry.period !== _.last(array).period; }); + var array = this.getAggregatedCandleSticks(candleStickSize); + array = _.filter(array, function(entry){ return entry.period !== _.last(array).period; }); - return _.last(array); + return _.last(array); }; storage.prototype.getAggregatedCandleSticks = function(candleStickSize) { - var candleStickArray = this.selectCollection(candleStickSize); + var candleStickArray = this.selectCollection(candleStickSize); - var candleStickSizeSeconds = 60 * candleStickSize; + var candleStickSizeSeconds = 60 * candleStickSize; - var latestAggregatedPeriod = this.getLastNonEmptyPeriod(candleStickSize) - candleStickSizeSeconds; + var latestAggregatedPeriod = this.getLastNonEmptyPeriod(candleStickSize) - candleStickSizeSeconds; - var candleSticks = this.getAllCandlesSince(latestAggregatedPeriod, 1); + var candleSticks = this.getAllCandlesSince(latestAggregatedPeriod, 1); - var startTimeStamp = (Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds) + candleStickSizeSeconds; - var stopTimeStamp = _.last(candleSticks).period; + var startTimeStamp = (Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds) + candleStickSizeSeconds; + var stopTimeStamp = _.last(candleSticks).period; - for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { + for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { - var beginPeriod = i; - var endPeriod = beginPeriod + candleStickSizeSeconds; + var beginPeriod = i; + var endPeriod = beginPeriod + candleStickSizeSeconds; - var currentCandleStick = {'period':beginPeriod,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + var currentCandleStick = {'period':beginPeriod,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - var relevantSticks = _.filter(candleSticks, function(candleStick) { + var relevantSticks = _.filter(candleSticks, function(candleStick) { - return candleStick.period >= beginPeriod && candleStick.period < endPeriod; + return candleStick.period >= beginPeriod && candleStick.period < endPeriod; - },this); + },this); - currentCandleStick.open = relevantSticks[0].open; - currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; - currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; - currentCandleStick.close = relevantSticks[relevantSticks.length - 1].close; - currentCandleStick.volume = _.reduce(relevantSticks, function(memo, entry) { return Number(BigNumber(memo).plus(BigNumber(entry.volume)).round(8)); }, 0); - if(currentCandleStick.volume === 0) { - currentCandleStick.vwap = currentCandleStick.close; - } else { - currentCandleStick.vwap = Number(BigNumber(_.reduce(relevantSticks, function(memo, entry) { + currentCandleStick.open = relevantSticks[0].open; + currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; + currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; + currentCandleStick.close = relevantSticks[relevantSticks.length - 1].close; + currentCandleStick.volume = _.reduce(relevantSticks, function(memo, entry) { return Number(BigNumber(memo).plus(BigNumber(entry.volume)).round(8)); }, 0); + if(currentCandleStick.volume === 0) { + currentCandleStick.vwap = currentCandleStick.close; + } else { + currentCandleStick.vwap = Number(BigNumber(_.reduce(relevantSticks, function(memo, entry) { - return Number(BigNumber(memo).plus(BigNumber(entry.vwap).times(BigNumber(entry.volume))).round(2)); + return Number(BigNumber(memo).plus(BigNumber(entry.vwap).times(BigNumber(entry.volume))).round(2)); - }, 0)).dividedBy(currentCandleStick.volume).round(2)); - } + }, 0)).dividedBy(currentCandleStick.volume).round(2)); + } - this.push(currentCandleStick, candleStickSize); + this.push(currentCandleStick, candleStickSize); - } + } - return candleStickArray.candleSticks; + return candleStickArray.candleSticks; }; storage.prototype.materialise = function(callback) { - var candleStickArray = this.selectCollection(1); + var candleStickArray = this.selectCollection(1); - var csDatastore = db(config.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csDatastore = db(config.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); - csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { + csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { - var filterPeriod = 0 + var filterPeriod = 0 - if(!err && sticks.length > 0) { + if(!err && sticks.length > 0) { - filterPeriod = sticks[0].period; + filterPeriod = sticks[0].period; - } + } - materialiseCs = _.filter(candleStickArray.candleSticks, function(cs){ + materialiseCs = _.filter(candleStickArray.candleSticks, function(cs){ - return cs.period >= filterPeriod + return cs.period >= filterPeriod; - }); + }); - if(materialiseCs.length > 0) { + if(materialiseCs.length > 0) { - csCollection.remove({ period: { $gte: filterPeriod } }, function(err, resp) { + csCollection.remove({ period: { $gte: filterPeriod } }, function(err, resp) { - if(err) { + if(err) { - csDatastore.close(); + csDatastore.close(); - callback(err); + callback(err); - } else { + } else { - csCollection.insert(materialiseCs, function(err) { + csCollection.insert(materialiseCs, function(err) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err); + callback(err); - } else { + } else { - callback(null); + callback(null); - } + } - }); + }); - } - - }); + } - } else { + }); - callback(null); + } else { - } + callback(null); - }); + } + + }); }; storage.prototype.removeOldDBCandles = function(filterPeriod) { - var csDatastore = db(config.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csDatastore = db(config.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); - csCollection.remove({ period: { $lte: filterPeriod } }, function(err, resp) { + csCollection.remove({ period: { $lte: filterPeriod } }, function(err, resp) { - csDatastore.close(); + csDatastore.close(); - }); + }); }; storage.prototype.getDBCandles = function(callback) { - var csDatastore = db(config.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csDatastore = db(config.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); - csCollection.ensureIndex({period: 1}); + csCollection.ensureIndex({period: 1}); - csCollection.find({}).sort({period:1}, function(err, candleSticks) { + csCollection.find({}).sort({period:1}, function(err, candleSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err); + callback(err); - } else if(candleSticks.length > 0 ){ + } else if(candleSticks.length > 0 ){ - var storageCandleSticks = _.map(candleSticks, function(candleStick){ - return {'period':candleStick.period,'open':candleStick.open,'high':candleStick.high,'low':candleStick.low,'close':candleStick.close,'volume':candleStick.volume, 'vwap':candleStick.vwap}; - }); + var storageCandleSticks = _.map(candleSticks, function(candleStick){ + return {'period':candleStick.period, 'open':candleStick.open, 'high':candleStick.high, 'low':candleStick.low, 'close':candleStick.close, 'volume':candleStick.volume, 'vwap':candleStick.vwap}; + }); - this.set(storageCandleSticks); + this.set(storageCandleSticks); - callback(null); + callback(null); - } else { + } else { - callback(null); + callback(null); - } + } - }.bind(this)); + }.bind(this)); }; var candlestorage = new storage(); -module.exports = candlestorage; \ No newline at end of file +module.exports = candlestorage; diff --git a/services/dataprocessor.js b/services/dataprocessor.js index 44db5d1..d6977c8 100644 --- a/services/dataprocessor.js +++ b/services/dataprocessor.js @@ -7,11 +7,11 @@ var tools = require('./tools.js'); var dataprocessor = function(candleStickSize) { - this.candleStickSize = candleStickSize; + this.candleStickSize = candleStickSize; - this.initialDBWriteDone = false; + this.initialDBWriteDone = false; - _.bindAll(this, 'updateCandleStick', 'createBaseCandleSticks', 'processInitialLoad', 'processUpdate', 'initialize', 'updateCandleDB'); + _.bindAll(this, 'updateCandleStick', 'createBaseCandleSticks', 'processInitialLoad', 'processUpdate', 'initialize', 'updateCandleDB'); }; @@ -23,207 +23,209 @@ Util.inherits(dataprocessor, EventEmitter); dataprocessor.prototype.updateCandleStick = function (candleStick, tick) { - if(!candleStick.open) { + if(!candleStick.open) { - candleStick.open = tick.price; - candleStick.high = tick.price; - candleStick.low = tick.price; - candleStick.close = tick.price; - candleStick.volume = tick.amount; - candleStick.vwap = tick.price; + candleStick.open = tick.price; + candleStick.high = tick.price; + candleStick.low = tick.price; + candleStick.close = tick.price; + candleStick.volume = tick.amount; + candleStick.vwap = tick.price; - } else { + } else { - var currentVwap = BigNumber(candleStick.vwap).times(BigNumber(candleStick.volume)); - var newVwap = BigNumber(tick.price).times(BigNumber(tick.amount)); + var currentVwap = BigNumber(candleStick.vwap).times(BigNumber(candleStick.volume)); + var newVwap = BigNumber(tick.price).times(BigNumber(tick.amount)); - candleStick.high = _.max([candleStick.high, tick.price]); - candleStick.low = _.min([candleStick.low, tick.price]); + candleStick.high = _.max([candleStick.high, tick.price]); + candleStick.low = _.min([candleStick.low, tick.price]); - candleStick.volume = Number(BigNumber(candleStick.volume).plus(BigNumber(tick.amount)).round(8)); - candleStick.vwap = Number(currentVwap.plus(newVwap).dividedBy(BigNumber(candleStick.volume)).round(2)); + candleStick.volume = Number(BigNumber(candleStick.volume).plus(BigNumber(tick.amount)).round(8)); + candleStick.vwap = Number(currentVwap.plus(newVwap).dividedBy(BigNumber(candleStick.volume)).round(2)); - } + } - candleStick.close = tick.price; + candleStick.close = tick.price; - return candleStick; + return candleStick; }; dataprocessor.prototype.createBaseCandleSticks = function (callback) { - if(this.ticks.length > 0) { + var previousClose = 0; - var candleStickSizeSeconds = 60; + if(this.ticks.length > 0) { - var tickTimeStamp = this.ticks[0].date; + var candleStickSizeSeconds = 60; - var lastStoragePeriod = storage.getLastNonEmptyPeriod(); - var firstTickCandleStick = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); + var tickTimeStamp = this.ticks[0].date; - if(lastStoragePeriod < firstTickCandleStick && lastStoragePeriod !== 0) { - tickTimeStamp = lastStoragePeriod + candleStickSizeSeconds; - } + var lastStoragePeriod = storage.getLastNonEmptyPeriod(); + var firstTickCandleStick = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); - var now = tools.unixTimeStamp(new Date().getTime()); + if(lastStoragePeriod < firstTickCandleStick && lastStoragePeriod !== 0) { + tickTimeStamp = lastStoragePeriod + candleStickSizeSeconds; + } - var startTimeStamp = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); - var stopTimeStamp = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); + var now = tools.unixTimeStamp(new Date().getTime()); - var endTimeStamp = startTimeStamp + candleStickSizeSeconds; + var startTimeStamp = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); + var stopTimeStamp = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); - while(endTimeStamp < this.ticks[0].date) { + var endTimeStamp = startTimeStamp + candleStickSizeSeconds; - var previousClose = storage.getLastNonEmptyClose(); + while(endTimeStamp < this.ticks[0].date) { - storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + previousClose = storage.getLastNonEmptyClose(); - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); - } + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - var currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + } - this.ticks.forEach(function(tick){ + var currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - tickTimeStamp = tick.date; + this.ticks.forEach(function(tick){ - while(tickTimeStamp >= endTimeStamp + candleStickSizeSeconds) { + tickTimeStamp = tick.date; - if(currentCandleStick.volume > 0) { - storage.push(currentCandleStick); - } + while(tickTimeStamp >= endTimeStamp + candleStickSizeSeconds) { - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + if(currentCandleStick.volume > 0) { + storage.push(currentCandleStick); + } - var previousClose = storage.getLastNonEmptyClose(); + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + previousClose = storage.getLastNonEmptyClose(); - } + storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); - if(tickTimeStamp >= endTimeStamp) { + } - if(currentCandleStick.volume > 0) { - storage.push(currentCandleStick); - } + if(tickTimeStamp >= endTimeStamp) { - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + if(currentCandleStick.volume > 0) { + storage.push(currentCandleStick); + } - currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - } + currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - if(tickTimeStamp >= startTimeStamp && tickTimeStamp < endTimeStamp) { + } - currentCandleStick = this.updateCandleStick(currentCandleStick,tick); + if(tickTimeStamp >= startTimeStamp && tickTimeStamp < endTimeStamp) { - } + currentCandleStick = this.updateCandleStick(currentCandleStick,tick); - }.bind(this)); + } - if(currentCandleStick.volume > 0) { + }.bind(this)); - storage.push(currentCandleStick); + if(currentCandleStick.volume > 0) { - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + storage.push(currentCandleStick); - } + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { + } - var beginPeriod = i; - var endPeriod = beginPeriod + candleStickSizeSeconds; + for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { - var previousClose = storage.getLastNonEmptyClose(); + var beginPeriod = i; + var endPeriod = beginPeriod + candleStickSizeSeconds; - storage.push({'period':beginPeriod,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + previousClose = storage.getLastNonEmptyClose(); - } + storage.push({'period':beginPeriod,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); - callback(null); + } - } else { + callback(null); - callback(null); + } else { - } + callback(null); + + } }; dataprocessor.prototype.processInitialLoad = function(err, result) { - if(err) { + if(err) { - logger.log('Couldn\'t create candlesticks due to a database error'); - logger.error(err.stack); + logger.error('Couldn\'t create candlesticks due to a database error'); + logger.error(err.stack); - process.exit(); + process.exit(); - } else { + } else { - this.emit('initialized'); + this.emit('initialized'); - } + } }; dataprocessor.prototype.processUpdate = function(err, result) { - this.ticks = []; + this.ticks = []; - if(err) { + if(err) { - logger.error('Couldn\'t create candlesticks due to a database error'); - logger.error(err.stack); + logger.error('Couldn\'t create candlesticks due to a database error'); + logger.error(err.stack); - process.exit(); + process.exit(); - } else { + } else { - var latestCandleStick = storage.getLastNCandles(1)[0]; + var latestCandleStick = storage.getLastNCandles(1)[0]; - if(!this.initialDBWriteDone) { - this.emit('initialDBWrite'); - this.initialDBWriteDone = true; - } else { + if(!this.initialDBWriteDone) { + this.emit('initialDBWrite'); + this.initialDBWriteDone = true; + } else { - this.emit('update', latestCandleStick); - - } + this.emit('update', latestCandleStick); } + } + }; dataprocessor.prototype.initialize = function() { - async.waterfall([ - storage.getDBCandles + async.waterfall([ + storage.getDBCandles ], this.processInitialLoad); -}; + }; dataprocessor.prototype.updateCandleDB = function(ticks) { - var period = storage.getLastNonEmptyPeriod(); + var period = storage.getLastNonEmptyPeriod(); - this.ticks = _.filter(ticks,function(tick){ + this.ticks = _.filter(ticks,function(tick){ - return tick.date >= period; + return tick.date >= period; - }); + }); - async.waterfall([ - this.createBaseCandleSticks, - storage.materialise + async.waterfall([ + this.createBaseCandleSticks, + storage.materialise ], this.processUpdate); }; -module.exports = dataprocessor; \ No newline at end of file +module.exports = dataprocessor; diff --git a/services/dataretriever.js b/services/dataretriever.js index 035297d..e735c50 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -5,9 +5,9 @@ var api = require('./api.js'); var downloader = function(refreshInterval){ - this.refreshInterval = refreshInterval; + this.refreshInterval = refreshInterval; - _.bindAll(this, 'start', 'stop', 'processTrades'); + _.bindAll(this, 'start', 'stop', 'processTrades'); }; @@ -19,32 +19,32 @@ Util.inherits(downloader, EventEmitter); downloader.prototype.processTrades = function(err, trades) { - if(!err) { + if(!err) { - this.emit('update', trades); + this.emit('update', trades); - } + } }; downloader.prototype.start = function() { - logger.log('Downloader started!'); - + logger.log('Downloader started!'); + + api.getTrades(this.processTrades); + + this.downloadInterval = setInterval(function(){ api.getTrades(this.processTrades); - - this.downloadInterval = setInterval(function(){ - api.getTrades(this.processTrades); - }.bind(this),1000 * this.refreshInterval); + }.bind(this),1000 * this.refreshInterval); }; downloader.prototype.stop = function() { - clearInterval(this.downloadInterval); + clearInterval(this.downloadInterval); - logger.log('Downloader stopped!'); + logger.log('Downloader stopped!'); }; -module.exports = downloader; \ No newline at end of file +module.exports = downloader; diff --git a/services/loggingservice.js b/services/loggingservice.js index 29d5970..75868f1 100644 --- a/services/loggingservice.js +++ b/services/loggingservice.js @@ -7,40 +7,40 @@ var config = require('../config.js'); var logger = function() { - this.debugEnabled = config.debug; + this.debugEnabled = config.debug; - _.bindAll(this, 'log', 'debug', 'error'); + _.bindAll(this, 'log', 'debug', 'error'); }; logger.prototype.log = function(message) { - var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - - console.log('[' + now + '] (INFO) ' + message); + var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); + + console.log('[' + now + '] (INFO) ' + message); }; logger.prototype.debug = function(message) { - if(this.debugEnabled) { + if(this.debugEnabled) { - var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); + var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - console.log('[' + now + '] (DEBUG) ' + message); + console.log('[' + now + '] (DEBUG) ' + message); - } + } }; logger.prototype.error = function(message) { - var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); + var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - console.log('[' + now + '] (ERROR) ' + message); + console.log('[' + now + '] (ERROR) ' + message); }; var loggingservice = new logger(); -module.exports = loggingservice; \ No newline at end of file +module.exports = loggingservice; diff --git a/services/ordermonitor.js b/services/ordermonitor.js index 8aa2610..c26d5d3 100644 --- a/services/ordermonitor.js +++ b/services/ordermonitor.js @@ -5,10 +5,10 @@ var api = require('./api.js'); var ordermonitor = function() { - _.bindAll(this, 'checkCancellation', 'processCancellation', 'processSimulation', 'add', 'resolvePreviousOrder'); + _.bindAll(this, 'checkCancellation', 'processCancellation', 'processSimulation', 'add', 'resolvePreviousOrder'); + + this.checkOrder = {}; - this.checkOrder = {}; - }; //---EventEmitter Setup @@ -19,110 +19,110 @@ Util.inherits(ordermonitor, EventEmitter); ordermonitor.prototype.checkCancellation = function(checkOrder, filled) { - if(checkOrder.status !== 'filled') { - - if(filled) { + if(checkOrder.status !== 'filled') { - checkOrder.status = 'filled'; + if(filled) { - clearInterval(checkOrder.interval); - clearTimeout(checkOrder.timeout); + checkOrder.status = 'filled'; - logger.log('Order (' + checkOrder.id + ') filled succesfully!'); + clearInterval(checkOrder.interval); + clearTimeout(checkOrder.timeout); - this.emit('filled', checkOrder); + logger.log('Order (' + checkOrder.id + ') filled succesfully!'); - } + this.emit('filled', checkOrder); } + } + }; ordermonitor.prototype.processCancellation = function(checkOrder, cancelled) { - if(cancelled && checkOrder.status !== 'cancelled') { + if(cancelled && checkOrder.status !== 'cancelled') { - checkOrder.status = 'cancelled'; + checkOrder.status = 'cancelled'; - logger.log('Order (' + checkOrder.id + ') cancelled!'); + logger.log('Order (' + checkOrder.id + ') cancelled!'); - this.emit('cancelled', checkOrder); + this.emit('cancelled', checkOrder); - } else if(checkOrder.status !== 'filled') { + } else if(checkOrder.status !== 'filled') { - checkOrder.status = 'filled'; + checkOrder.status = 'filled'; - logger.log('Order (' + checkOrder.id + ') filled succesfully!'); + logger.log('Order (' + checkOrder.id + ') filled succesfully!'); - this.emit('filled', checkOrder); + this.emit('filled', checkOrder); - } + } }; ordermonitor.prototype.processSimulation = function(checkOrder) { - logger.log('Order (' + checkOrder.id + ') filled succesfully!'); + logger.log('Order (' + checkOrder.id + ') filled succesfully!'); - checkOrder.status = 'filled'; + checkOrder.status = 'filled'; - this.emit('filled', checkOrder); + this.emit('filled', checkOrder); }; ordermonitor.prototype.add = function(orderDetails, cancelTime) { - this.resolvePreviousOrder(); + this.resolvePreviousOrder(); - this.checkOrder = {id:orderDetails.order, orderDetails:orderDetails, status:'open'}; + this.checkOrder = {id:orderDetails.order, orderDetails:orderDetails, status:'open'}; - logger.log('Monitoring order: ' + this.checkOrder.id + ' (Cancellation after ' + cancelTime + ' minutes)'); + logger.log('Monitoring order: ' + this.checkOrder.id + ' (Cancellation after ' + cancelTime + ' minutes)'); - if(this.checkOrder.id === 'Simulated') { + if(this.checkOrder.id === 'Simulated') { - this.processSimulation(this.checkOrder); + this.processSimulation(this.checkOrder); - } else { + } else { - this.checkOrder.interval = setInterval(function() { + this.checkOrder.interval = setInterval(function() { - api.orderFilled(this.checkOrder.id, function(err, response){ - if(!err) { - this.checkCancellation(this.checkOrder, response); - } - }.bind(this)); + api.orderFilled(this.checkOrder.id, function(err, response){ + if(!err) { + this.checkCancellation(this.checkOrder, response); + } + }.bind(this)); - }.bind(this), 1000 * 10); + }.bind(this), 1000 * 10); - this.checkOrder.timeout = setTimeout(function() { + this.checkOrder.timeout = setTimeout(function() { - clearInterval(this.checkOrder.interval); + clearInterval(this.checkOrder.interval); - if(this.checkOrder.status === 'open') { + if(this.checkOrder.status === 'open') { - api.cancelOrder(this.checkOrder.id, function(err, response) { - this.processCancellation(this.checkOrder, response); - }.bind(this)); + api.cancelOrder(this.checkOrder.id, function(err, response) { + this.processCancellation(this.checkOrder, response); + }.bind(this)); - } + } - }.bind(this), 1000 * 60 * cancelTime); + }.bind(this), 1000 * 60 * cancelTime); - } + } }; ordermonitor.prototype.resolvePreviousOrder = function() { - if(this.checkOrder.status === 'open') { + if(this.checkOrder.status === 'open') { - clearInterval(this.checkOrder.interval); - clearTimeout(this.checkOrder.timeout); + clearInterval(this.checkOrder.interval); + clearTimeout(this.checkOrder.timeout); - this.checkOrder.status = 'cancelled'; + this.checkOrder.status = 'cancelled'; - } + } }; -module.exports = ordermonitor; \ No newline at end of file +module.exports = ordermonitor; diff --git a/services/pricemonitor.js b/services/pricemonitor.js index bc48a5e..745dccd 100644 --- a/services/pricemonitor.js +++ b/services/pricemonitor.js @@ -6,14 +6,14 @@ var storage = require('./candlestorage.js'); var pricemonitor = function(slPercentageB, slPercentageS, candleStickSizeMinutes) { - this.percentageBought = slPercentageB; - this.percentageSold = slPercentageS; - this.candleStickSizeMinutes = candleStickSizeMinutes; + this.percentageBought = slPercentageB; + this.percentageSold = slPercentageS; + this.candleStickSizeMinutes = candleStickSizeMinutes; - this.position = 'none'; + this.position = 'none'; + + _.bindAll(this, 'check', 'setPosition', 'update'); - _.bindAll(this, 'check', 'setPosition', 'update'); - }; //---EventEmitter Setup @@ -24,80 +24,82 @@ Util.inherits(pricemonitor, EventEmitter); pricemonitor.prototype.check = function(price) { - if(this.position === 'bought') { - - if(price <= this.checkPriceBought) { - logger.log('Stop Loss triggered (Long Entry: ' + this.posPrice + ' Exit: ' + price + ')'); - this.position = 'none'; - this.posPrice = 0; - this.emit('advice', 'sell'); - } + if(this.position === 'bought') { - } else if(this.position === 'sold') { + if(price <= this.checkPriceBought) { + logger.log('Stop Loss triggered (Long Entry: ' + this.posPrice + ' Exit: ' + price + ')'); + this.position = 'none'; + this.posPrice = 0; + this.emit('advice', 'sell'); + } + + } else if(this.position === 'sold') { - if(price >= this.checkPriceSold) { - logger.log('Stop Loss triggered (Short Entry: ' + this.posPrice + ' Exit: ' + price + ')'); - this.position = 'none'; - this.posPrice = 0; - this.emit('advice', 'buy'); - } + if(price >= this.checkPriceSold) { + logger.log('Stop Loss triggered (Short Entry: ' + this.posPrice + ' Exit: ' + price + ')'); + this.position = 'none'; + this.posPrice = 0; + this.emit('advice', 'buy'); + } - } else { + } else { - this.emit('advice', 'hold'); + this.emit('advice', 'hold'); - } + } }; pricemonitor.prototype.setPosition = function(pos, price) { - if(pos === 'bought') { + if(pos === 'bought') { - this.position = 'bought' - this.posPrice = price; - this.checkPriceBought = Number(BigNumber(this.posPrice).times(BigNumber(1).minus(BigNumber(this.percentageBought).dividedBy(BigNumber(100))))); + this.position = 'bought'; + this.posPrice = price; + this.checkPriceBought = Number(BigNumber(this.posPrice).times(BigNumber(1).minus(BigNumber(this.percentageBought).dividedBy(BigNumber(100))))); - } else if(pos === 'sold') { + } else if(pos === 'sold') { - this.position = 'sold'; - this.posPrice = price; - this.checkPriceSold = Number(BigNumber(this.posPrice).times(BigNumber(1).plus(BigNumber(this.percentageSold).dividedBy(BigNumber(100))))); + this.position = 'sold'; + this.posPrice = price; + this.checkPriceSold = Number(BigNumber(this.posPrice).times(BigNumber(1).plus(BigNumber(this.percentageSold).dividedBy(BigNumber(100))))); - } + } }; pricemonitor.prototype.update = function(cs) { - var diff = cs.close - cs.open; - var size = Math.abs(Number(BigNumber(cs.close).minus(BigNumber(cs.open)).round(2))); - var averageSize = storage.getAverageCandleStickSize(10, this.candleStickSizeMinutes); + var diff = cs.close - cs.open; + var size = Math.abs(Number(BigNumber(cs.close).minus(BigNumber(cs.open)).round(2))); + var averageSize = storage.getAverageCandleStickSize(10, this.candleStickSizeMinutes); - var change = Number(BigNumber(size).dividedBy(2).round(2)); + var change = Number(BigNumber(size).dividedBy(2).round(2)); - if(size >= averageSize * 2) { + var newSl; - if(this.position === 'bought' && diff > 0) { + if(size >= averageSize * 2) { - var newSl = Number(BigNumber(this.checkPriceBought).plus(change)); + if(this.position === 'bought' && diff > 0) { - logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); + newSl = Number(BigNumber(this.checkPriceBought).plus(change)); - this.checkPriceBought = newSl; + logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); - } else if(this.position === 'sold' && diff < 0) { + this.checkPriceBought = newSl; - var newSl = Number(BigNumber(this.checkPriceSold).minus(change)); + } else if(this.position === 'sold' && diff < 0) { - logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); + newSl = Number(BigNumber(this.checkPriceSold).minus(change)); - this.checkPriceSold = newSl; + logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); - } + this.checkPriceSold = newSl; } + } + }; -module.exports = pricemonitor; \ No newline at end of file +module.exports = pricemonitor; diff --git a/services/profitreporter.js b/services/profitreporter.js index 2c08b47..2ce0581 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -6,10 +6,10 @@ var api = require('./api.js'); var profitreporter = function(currencyPair) { - this.currencyPair = currencyPair; + this.currencyPair = currencyPair; + + _.bindAll(this, 'createReport', 'processBalance', 'updateBalance'); - _.bindAll(this, 'createReport', 'processBalance', 'updateBalance'); - }; //---EventEmitter Setup @@ -20,50 +20,50 @@ Util.inherits(profitreporter, EventEmitter); profitreporter.prototype.createReport = function() { - var report = this.currencyPair.asset + ': ' + this.assetBalance + ' ' + this.currencyPair.currency + ': ' + this.currencyBalance + ' Total in ' + this.currencyPair.currency + ': ' + this.totalCurrencyBalance + ' Profit: ' + this.profitAbsolute + ' (' + this.profitPercentage + '%)'; + var report = this.currencyPair.asset + ': ' + this.assetBalance + ' ' + this.currencyPair.currency + ': ' + this.currencyBalance + ' Total in ' + this.currencyPair.currency + ': ' + this.totalCurrencyBalance + ' Profit: ' + this.profitAbsolute + ' (' + this.profitPercentage + '%)'; - logger.log('Profit Report: ' + report); + logger.log('Profit Report: ' + report); - this.emit('report', report); + this.emit('report', report); }; profitreporter.prototype.processBalance = function(err, result) { - - this.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); - this.highestBid = _.first(result.orderBook.bids).currencyPrice; - this.assetBalanceInCurrency = BigNumber(this.assetBalance).times(BigNumber(this.highestBid)); + this.currencyBalance = parseFloat(result.balance.currencyAvailable); + this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); + + this.highestBid = _.first(result.orderBook.bids).currencyPrice; + this.assetBalanceInCurrency = BigNumber(this.assetBalance).times(BigNumber(this.highestBid)); - if(!this.totalCurrencyBalance) { - this.initalTotalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); - } + if(!this.totalCurrencyBalance) { + this.initalTotalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); + } - this.totalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); - this.profitAbsolute = Number(BigNumber(this.totalCurrencyBalance).minus(this.initalTotalCurrencyBalance)) - this.profitPercentage = Number(BigNumber(this.profitAbsolute).dividedBy(BigNumber(this.initalTotalCurrencyBalance)).times(BigNumber(100)).round(2)); + this.totalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); + this.profitAbsolute = Number(BigNumber(this.totalCurrencyBalance).minus(this.initalTotalCurrencyBalance)); + this.profitPercentage = Number(BigNumber(this.profitAbsolute).dividedBy(BigNumber(this.initalTotalCurrencyBalance)).times(BigNumber(100)).round(2)); - if(this.includeReport) { - this.createReport(); - } + if(this.includeReport) { + this.createReport(); + } - this.emit('update', {'asset': this.currencyPair.asset, 'currency': this.currencyPair.currency, 'currencyBalance': this.currencyBalance, 'assetBalance': this.assetBalance, 'profitAbsolute': this.profitAbsolute, 'profitPercentage': this.profitPercentage}); + this.emit('update', {'asset': this.currencyPair.asset, 'currency': this.currencyPair.currency, 'currencyBalance': this.currencyBalance, 'assetBalance': this.assetBalance, 'profitAbsolute': this.profitAbsolute, 'profitPercentage': this.profitPercentage}); }; profitreporter.prototype.updateBalance = function(includeReport) { - this.includeReport = includeReport; + this.includeReport = includeReport; - async.series( - { - balance: api.getBalance, - orderBook: api.getOrderBook - }, - this.processBalance - ); + async.series( + { + balance: api.getBalance, + orderBook: api.getOrderBook + }, + this.processBalance + ); }; -module.exports = profitreporter; \ No newline at end of file +module.exports = profitreporter; diff --git a/services/pushservice.js b/services/pushservice.js index 8c49eb9..625ae21 100644 --- a/services/pushservice.js +++ b/services/pushservice.js @@ -4,53 +4,53 @@ var logger = require('./loggingservice.js'); var pusher = function(pushOver) { - if(pushOver.pushUserId && pushOver.pushAppToken) { + if(pushOver.pushUserId && pushOver.pushAppToken) { - var userId = pushOver.pushUserId - var appToken = pushOver.pushAppToken; + var userId = pushOver.pushUserId; + var appToken = pushOver.pushAppToken; - this.p = new push( { - user: userId, - token: appToken - }); + this.p = new push( { + user: userId, + token: appToken + }); - this.configured = true; + this.configured = true; - } else { + } else { - this.configured = false; + this.configured = false; - } + } - _.bindAll(this, 'send'); + _.bindAll(this, 'send'); }; pusher.prototype.send = function(title, message, sound, priority) { - if(this.configured) { + if(this.configured) { - var msg = { - message: message, - title: title, - sound: title, // optional - priority: priority // optional - }; + var msg = { + message: message, + title: title, + sound: title, // optional + priority: priority // optional + }; - this.p.send( msg, function( err, result ) { - if ( err ) { - throw err; - } + this.p.send( msg, function( err, result ) { + if ( err ) { + throw err; + } - logger.log('Push notification sent!'); - }); + logger.log('Push notification sent!'); + }); - } else { + } else { - logger.log('Push Service Misconfigured'); + logger.log('Push Service Misconfigured'); - } + } }; -module.exports = pusher; \ No newline at end of file +module.exports = pusher; diff --git a/services/tools.js b/services/tools.js index 8cf2a8a..8cedab7 100644 --- a/services/tools.js +++ b/services/tools.js @@ -4,9 +4,9 @@ var tools = function() { }; tools.prototype.unixTimeStamp = function(timestamp) { - return Math.floor(timestamp/1000); + return Math.floor(timestamp/1000); }; var utiltools = new tools(); -module.exports = utiltools; \ No newline at end of file +module.exports = utiltools; diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index c45bc89..682ccd3 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -1,31 +1,24 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); var async = require('async'); var logger = require('./loggingservice.js'); var storage = require('./candlestorage.js'); +var fs = require('fs'); -var advisor = function(indicatorSettings, candleStickSize) { +var advisor = function(indicatorSettings, candleStickSize, backTesting) { - this.indicator = indicatorSettings.indicator; - this.options = indicatorSettings.options; - this.buyTreshold = indicatorSettings.buyTreshold; - this.sellTreshold = indicatorSettings.sellTreshold; this.candleStickSize = candleStickSize; + this.backTesting = backTesting; - if(this.indicator === 'MACD') { - this.selectedIndicator = this.calculateMacd; - } else if(this.indicator === 'PPO') { - this.selectedIndicator = this.calculatePPO; + if(fs.existsSync('./indicators/' + indicatorSettings.indicator + '.js')) { + var indicator = require('../indicators/' + indicatorSettings.indicator + '.js'); + this.selectedIndicator = new indicator(indicatorSettings.options); } else { - throw new Error('Wrong indicator chosen. This indicator doesn\'t exist.'); + var err = new Error('Wrong indicator chosen. This indicator doesn\'t exist.'); + logger.error(err.stack); + process.exit(); } - this.previousIndicatorResult = {}; - this.indicatorResult = {}; - - this.length = 0; - - _.bindAll(this, 'start', 'update', 'calculateEma', 'calculateMacd', 'calculatePPO', 'generateAdvice'); + _.bindAll(this, 'start', 'update'); }; @@ -41,99 +34,30 @@ advisor.prototype.start = function() { for(var i = 0; i < candleSticks.length; i++) { - this.length = this.length + 1; - - var usePrice = candleSticks[i].close; + var advice = this.selectedIndicator.calculate(candleSticks[i]); - this.previousIndicatorResult = this.indicatorResult; - this.indicatorResult = this.selectedIndicator(usePrice, this.options, this.previousIndicatorResult); - } }; advisor.prototype.update = function(cs) { - this.length = this.length + 1; - - var usePrice = cs.close; - - this.previousIndicatorResult = this.indicatorResult; - this.indicatorResult = this.selectedIndicator(usePrice, this.options, this.previousIndicatorResult); - - if(this.length >= this.options.neededPeriods) { - - this.generateAdvice(); + var advice = this.selectedIndicator.calculate(cs); + if(!this.backTesting) { + logger.log('Advice: ' + advice); } else { - - logger.log('Indicator needs ' + (this.options.neededPeriods - this.length) + ' more periods before actual trades will be executed.'); - + logger.debug('Advice: ' + advice); } -}; - -advisor.prototype.calculateEma = function(periods, priceToday, previousEma) { - - if(!previousEma) { - previousEma = priceToday; - } - - var k = BigNumber(2).dividedBy(BigNumber(periods+1)); - var ema = (BigNumber(priceToday).times(k)).plus(BigNumber(previousEma).times(BigNumber(1).minus(k))); - - return BigNumber(ema).round(8); - -}; - -advisor.prototype.calculateMacd = function(usePrice, options, previousMacd) { - - var emaLong = Number(this.calculateEma(options.longPeriods, usePrice, previousMacd.emaLong)); - var emaShort = Number(this.calculateEma(options.shortPeriods, usePrice, previousMacd.emaShort)); - - var macd = Number(BigNumber(emaShort).minus(BigNumber(emaLong))); - var macdSignal = Number(this.calculateEma(options.emaPeriods, macd, previousMacd.macdSignal)); - var macdHistogram = Number(BigNumber(macd).minus(BigNumber(macdSignal)).round(2)); - - return {'emaLong': emaLong, 'emaShort': emaShort, 'macd': macd, 'macdSignal': macdSignal, 'result': macdHistogram}; - -} - -advisor.prototype.calculatePPO = function(usePrice, options, previousPPO) { - - var emaLong = Number(this.calculateEma(options.longPeriods, usePrice, previousPPO.emaLong)); - var emaShort = Number(this.calculateEma(options.shortPeriods, usePrice, previousPPO.emaShort)); - - var PPO = Number(BigNumber(emaShort).minus(BigNumber(emaLong)).dividedBy(BigNumber(emaLong)).times(BigNumber(100)).round(8)); - var PPOSignal = Number(this.calculateEma(options.emaPeriods, PPO, previousPPO.PPOSignal)); - var PPOHistogram = Number(BigNumber(PPO).minus(BigNumber(PPOSignal)).round(2)); - - return {'emaLong': emaLong, 'emaShort': emaShort, 'PPO': PPO, 'PPOSignal': PPOSignal, 'result': PPOHistogram}; - -} - -advisor.prototype.generateAdvice = function() { - - var advice; - - if(this.previousIndicatorResult.result <= this.buyTreshold && this.indicatorResult.result > this.buyTreshold) { - - advice = 'buy'; - - } else if(this.previousIndicatorResult.result >= this.sellTreshold && this.indicatorResult.result < this.sellTreshold) { - - advice = 'sell'; - + if(['buy', 'sell', 'hold'].indexOf(advice) >= 0) { + this.emit('advice', advice); } else { - - advice = 'hold'; - + var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); + logger.error(err.stack); + process.exit(); } - logger.debug('Advice: ' + advice + ' (Previous Indicator Result: ' + this.previousIndicatorResult.result + ' Current Indicator Result: ' + this.indicatorResult.result + ')'); - - this.emit('advice', advice); - }; -module.exports = advisor; \ No newline at end of file +module.exports = advisor; diff --git a/services/tradingagent.js b/services/tradingagent.js index ef4638b..c55935a 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -39,8 +39,8 @@ tradingagent.prototype.order = function(orderType) { } else { this.placeSimulatedOrder(); } - - } + + }; async.series( { @@ -65,7 +65,7 @@ tradingagent.prototype.calculateOrder = function(result) { var maxClose = Number(BigNumber(lastClose).times(BigNumber(1.0025)).round(2)); logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.tradingFee +')'); - + if(this.orderDetails.orderType === 'buy') { //var lowestAsk = _.first(orderBook.asks)[0]; @@ -83,10 +83,10 @@ tradingagent.prototype.calculateOrder = function(result) { var balance = (BigNumber(this.orderDetails.currencyBalance).minus(BigNumber(this.tradingReserveCurrency))).times(BigNumber(1).minus(BigNumber(this.orderDetails.tradingFee).dividedBy(BigNumber(100)))); logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); - + this.orderDetails.price = lowestAskWithSlippage; this.orderDetails.amount = Number(balance.dividedBy(BigNumber(this.orderDetails.price)).minus(BigNumber(0.005)).round(2)); - + } else if(this.orderDetails.orderType === 'sell') { //var highestBid = _.first(orderBook.bids)[0]; @@ -103,24 +103,24 @@ tradingagent.prototype.calculateOrder = function(result) { var highestBidWithSlippage = Number(BigNumber(highestBid).times(BigNumber(1).minus(BigNumber(this.slippagePercentage).dividedBy(BigNumber(100)))).round(2)); logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); - + this.orderDetails.price = highestBidWithSlippage; this.orderDetails.amount = Number(BigNumber(this.orderDetails.assetBalance).minus(BigNumber(this.tradingReserveAsset))); - + } }; tradingagent.prototype.placeRealOrder = function() { - + if(this.orderDetails.amount <= 0) { - logger.log('Insufficient funds to place an order.') + logger.log('Insufficient funds to place an order.'); } else { api.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, this.processOrder); - + } }; @@ -129,10 +129,12 @@ tradingagent.prototype.placeSimulatedOrder = function() { if(this.orderDetails.amount <= 0) { - logger.log('Insufficient funds to place an order.') + logger.log('Insufficient funds to place an order.'); } else { + this.orderDetails.order = 'Simulated'; + logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); this.emit('simulatedOrder', this.orderDetails); @@ -154,9 +156,9 @@ tradingagent.prototype.processOrder = function(err, order) { logger.log('Placed ' + this.orderDetails.orderType + ' order: ' + this.orderDetails.order + ' (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); this.emit('realOrder', this.orderDetails); - + } }; -module.exports = tradingagent; \ No newline at end of file +module.exports = tradingagent; From 712f543a04fb2caf17b2ad9c15f72e8c5105b876 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 1 Aug 2014 08:25:09 +0200 Subject: [PATCH 02/57] Bugfix and extra documentation --- README.md | 3 +++ indicators/template | 3 +++ package.json | 2 +- services/api.js | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a089ca0..46c6e9a 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ As of version 0.7 BitBot now uses indicators as small plugins. You can create yo // Insert your calculation logic here + // When done you should always return either 'buy', 'sell' or 'hold' + return advice; + }; module.exports = indicator; diff --git a/indicators/template b/indicators/template index 13dbc24..790294c 100644 --- a/indicators/template +++ b/indicators/template @@ -25,6 +25,9 @@ indicator.prototype.calculate = function(cs) { // Insert your calculation logic here + // When done you should always return either 'buy', 'sell' or 'hold' + return advice; + }; module.exports = indicator; diff --git a/package.json b/package.json index 49bec13..d888203 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.6.0", + "version": "0.7.0", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/api.js b/services/api.js index 4d8a777..46f437a 100644 --- a/services/api.js +++ b/services/api.js @@ -29,7 +29,7 @@ var api = function() { logger.error('Invalid exchange, exiting!'); return process.exit(); - + } this.q = async.queue(function (task, callback) { @@ -80,7 +80,7 @@ api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, cb) { } else { logger.error('Couldn\'t connect to the API.'); - cb(err, result); + //cb(err, result); return logger.error(JSON.stringify(err).substring(0,99)); } From 1a260e7aed85d8434c0c5c2768692efb03f01871 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Thu, 7 Aug 2014 23:59:07 +0200 Subject: [PATCH 03/57] v0.7.1 Backtester now monitors profit lost on fees Backtester now monitors how much of the total profit is lost on transaction fees. --- app.js | 2 +- backtester.js | 10 +++++++++- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index b7bc89a..e387427 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.0'); +console.log('Starting BitBot v0.7.1'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index d39c271..23a6c99 100644 --- a/backtester.js +++ b/backtester.js @@ -31,6 +31,8 @@ var totalBalanceInBTC = 0; var profit = 0; var profitPercentage = 0; var transactionFee = 0; +var totalFeeCosts = 0; +var totalFeeCostsPercentage = 0; var transactions = 0; var slTransactions = 0; var lastClose = 0; @@ -39,7 +41,7 @@ var csPeriod = 0; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.0'); +console.log('Starting BitBot Back-Tester v0.7.1'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -52,6 +54,8 @@ var createOrder = function(type, stopLoss) { usableBalance = Number(BigNumber(USDBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(USDBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).round(2))); + BTCBalance = Number(BigNumber(BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(lastClose)).round(2))); USDBalance = 0; @@ -70,6 +74,8 @@ var createOrder = function(type, stopLoss) { usableBalance = Number(BigNumber(BTCBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(BTCBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).times(lastClose).round(2))); + USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastClose)).round(2))); BTCBalance = 0; @@ -140,6 +146,7 @@ processor.on('initialized', function(){ totalBalanceInBTC = Number(BigNumber(BTCBalance).plus(BigNumber(USDBalance).dividedBy(BigNumber(lastClose))).round(2)); profit = Number(BigNumber(totalBalanceInUSD).minus(BigNumber(initialBalance)).round(2)); profitPercentage = Number(BigNumber(profit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); + totalFeeCostsPercentage = Number(BigNumber(totalFeeCosts).dividedBy(BigNumber(profit)).times(BigNumber(100)).round(2)); logger.log('----------Report----------'); logger.log('Transaction Fee: ' + transactionFee + '%'); @@ -148,6 +155,7 @@ processor.on('initialized', function(){ logger.log('Final Balance: ' + totalBalanceInUSD); logger.log('Final Balance BTC: ' + totalBalanceInBTC); logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); + logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); logger.log('Open Price: ' + _.first(loopArray).open); logger.log('Close Price: ' + _.last(loopArray).close); logger.log('Transactions: ' + transactions); diff --git a/package.json b/package.json index d888203..1e58173 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.0", + "version": "0.7.1", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { From efe45b509caa40f622d90be5b279e75825c7b90b Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 8 Aug 2014 00:10:49 +0200 Subject: [PATCH 04/57] Fixed lost on fee percentage calculation --- backtester.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtester.js b/backtester.js index 23a6c99..ec4a707 100644 --- a/backtester.js +++ b/backtester.js @@ -146,7 +146,7 @@ processor.on('initialized', function(){ totalBalanceInBTC = Number(BigNumber(BTCBalance).plus(BigNumber(USDBalance).dividedBy(BigNumber(lastClose))).round(2)); profit = Number(BigNumber(totalBalanceInUSD).minus(BigNumber(initialBalance)).round(2)); profitPercentage = Number(BigNumber(profit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); - totalFeeCostsPercentage = Number(BigNumber(totalFeeCosts).dividedBy(BigNumber(profit)).times(BigNumber(100)).round(2)); + totalFeeCostsPercentage = Number(BigNumber(totalFeeCosts).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); logger.log('----------Report----------'); logger.log('Transaction Fee: ' + transactionFee + '%'); From c23710b0d59326bbefae6ee8b9c4da39cb0717ef Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 8 Aug 2014 11:18:48 +0200 Subject: [PATCH 05/57] v0.7.2 DBHealth and extra backtester functionality --- app.js | 2 +- backtester.js | 48 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- tests/dbhealth.js | 32 +++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tests/dbhealth.js diff --git a/app.js b/app.js index e387427..66bc132 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.1'); +console.log('Starting BitBot v0.7.2'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index ec4a707..0653526 100644 --- a/backtester.js +++ b/backtester.js @@ -1,4 +1,6 @@ var _ = require('underscore'); +var BigNumber = require('bignumber.js'); +var moment = require('moment'); var dataprocessor = require('./services/dataprocessor.js'); var tradingadvisor = require('./services/tradingadvisor.js'); @@ -7,7 +9,6 @@ var storage = require('./services/candlestorage.js'); var logger = require('./services/loggingservice.js'); var pricemonitor = require('./services/pricemonitor.js'); var api = require('./services/api.js'); -var BigNumber = require('bignumber.js'); //------------------------------Config var config = require('./config.js'); @@ -30,6 +31,8 @@ var totalBalanceInUSD = 0; var totalBalanceInBTC = 0; var profit = 0; var profitPercentage = 0; +var bhProfit = 0; +var bhProfitPercentage = 0; var transactionFee = 0; var totalFeeCosts = 0; var totalFeeCostsPercentage = 0; @@ -37,11 +40,23 @@ var transactions = 0; var slTransactions = 0; var lastClose = 0; var csPeriod = 0; +var entryUSD = 0; +var exitUSD = 0; +var winners = 0; +var losers = 0; +var bigWinner = 0; +var bigLoser = 0; +var totalGain = 0; +var totalLoss = 0; +var averageGain = 0; +var averageLoss = 0; +var startDate; +var endDate; //------------------------------IntializeVariables //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.1'); +console.log('Starting BitBot Back-Tester v0.7.2'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -52,6 +67,8 @@ var createOrder = function(type, stopLoss) { if(type === 'buy' && USDBalance !== 0) { + entryUSD = USDBalance; + usableBalance = Number(BigNumber(USDBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(USDBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).round(2))); @@ -79,6 +96,20 @@ var createOrder = function(type, stopLoss) { USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastClose)).round(2))); BTCBalance = 0; + exitUSD = USDBalance; + + var tradeResult = Number(BigNumber(exitUSD).minus(BigNumber(entryUSD)).round(2)); + + if(exitUSD > entryUSD) { + winners += 1; + totalGain = Number(BigNumber(totalGain).plus(BigNumber(tradeResult)).round(2)); + if(tradeResult > bigWinner) {bigWinner = tradeResult;} + } else { + losers += 1; + totalLoss = Number(BigNumber(totalLoss).plus(BigNumber(tradeResult)).round(2)); + if(tradeResult < bigLoser) {bigLoser = tradeResult;} + } + transactions += 1; if(stopLoss) { @@ -147,6 +178,14 @@ processor.on('initialized', function(){ profit = Number(BigNumber(totalBalanceInUSD).minus(BigNumber(initialBalance)).round(2)); profitPercentage = Number(BigNumber(profit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); totalFeeCostsPercentage = Number(BigNumber(totalFeeCosts).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); + bhProfit = Number(BigNumber(_.last(loopArray).close).minus(_.first(loopArray).open).round(2)); + bhProfitPercentage = Number(BigNumber(bhProfit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); + + startDate = moment(new Date(_.first(loopArray).period*1000)).format('DD-MM-YYYY HH:mm:ss'); + endDate = moment(new Date(_.last(loopArray).period*1000)).format('DD-MM-YYYY HH:mm:ss'); + + averageGain = Number(BigNumber(totalGain).dividedBy(winners).round(2)); + averageLoss = Number(BigNumber(totalLoss).dividedBy(losers).round(2)); logger.log('----------Report----------'); logger.log('Transaction Fee: ' + transactionFee + '%'); @@ -154,10 +193,15 @@ processor.on('initialized', function(){ logger.log('Initial Balance BTC: ' + intialBalanceBTC); logger.log('Final Balance: ' + totalBalanceInUSD); logger.log('Final Balance BTC: ' + totalBalanceInBTC); + logger.log('Winning trades : ' + winners + ' Losing trades: ' + losers); + logger.log('Biggest winner: ' + bigWinner + ' Biggest loser: ' + bigLoser); + logger.log('Average winner: ' + averageGain + ' Average loser: ' + averageLoss); logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); + logger.log('Buy and Hold Profit: ' + bhProfit + ' (' + bhProfitPercentage + '%)'); logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); logger.log('Open Price: ' + _.first(loopArray).open); logger.log('Close Price: ' + _.last(loopArray).close); + logger.log('Start - End Date: ' + startDate + ' - ' + endDate); logger.log('Transactions: ' + transactions); logger.log('Stop Loss Transactions: ' + slTransactions); logger.log('--------------------------'); diff --git a/package.json b/package.json index 1e58173..b2036b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.1", + "version": "0.7.2", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/tests/dbhealth.js b/tests/dbhealth.js new file mode 100644 index 0000000..2ef4e5b --- /dev/null +++ b/tests/dbhealth.js @@ -0,0 +1,32 @@ +var _ = require('underscore'); +var config = require('../config.js'); +var storage = require('../services/candlestorage.js'); +var dataprocessor = require('../services/dataprocessor.js'); +var processor = new dataprocessor(config.candleStickSizeMinutes); + +processor.on('initialized', function(){ + + var loopArray = storage.getAllCandlesSince(); + + var testPeriod = _.first(loopArray).period - 60; + var success = true; + + _.each(loopArray, function(cs) { + + if(cs.period !== testPeriod + 60) { + success = false; + } + + testPeriod += 60; + + }); + + if(success) { + console.log("Database OK!"); + } else { + console.log("Database corrupt, empty your database and try collection historical information again"); + } + +}); + +processor.initialize(); From 51e15e6a209f8830cd0a34d009395a45cd730d91 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 8 Aug 2014 13:48:27 +0200 Subject: [PATCH 06/57] v0.7.3 Indicators are now aware of position Indicators are now aware of the current trading position (bought/sold) and fixed a bug in the DBHealth test. --- README.md | 17 ++++++++++++++--- app.js | 4 +++- backtester.js | 4 +++- indicators/MACD.js | 9 ++++++++- indicators/PPO.js | 9 ++++++++- indicators/template | 13 ++++++++++++- package.json | 2 +- services/tradingadvisor.js | 8 +++++++- tests/dbhealth.js | 8 +++++++- 9 files changed, 63 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 46c6e9a..9e4d792 100644 --- a/README.md +++ b/README.md @@ -73,14 +73,15 @@ Please read up on the following articles to help you choose the settings that be As of version 0.7 BitBot now uses indicators as small plugins. You can create your own indicator by using the following template: - var _ = require('underscore'); - var BigNumber = require('bignumber.js'); +var _ = require('underscore'); +var BigNumber = require('bignumber.js'); var indicator = function(options) { this.options = options; + this.position = {}; - _.bindAll(this, 'calculate'); + _.bindAll(this, 'calculate', 'setPosition'); // indicatorOptions // options: {The options required for your indicator to work} @@ -105,6 +106,16 @@ As of version 0.7 BitBot now uses indicators as small plugins. You can create yo }; + indicator.prototype.setPosition = function(pos) { + + // This function is required and shouldn't be changed unless you know what you are doing. + // Provides the indicator with information qbout the current position. + // {pos: position, price: price} + + this.position = pos; + + }; + module.exports = indicator; For examples on how to use this template, go have a look at one of the existing indicators in the indicators folder. diff --git a/app.js b/app.js index 66bc132..9d89efd 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.2'); +console.log('Starting BitBot v0.7.3'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); @@ -117,10 +117,12 @@ monitor.on('filled', function(order) { if(order.orderDetails.orderType === 'buy') { pricemon.setPosition('bought', order.orderDetails.price); + advisor.setPosition({pos: 'bought', price: order.orderDetails.price}); } else if(order.orderDetails.orderType === 'sell') { pricemon.setPosition('sold', order.orderDetails.price); + advisor.setPosition({pos: 'sold', price: order.orderDetails.price}); } diff --git a/backtester.js b/backtester.js index 0653526..8ba688e 100644 --- a/backtester.js +++ b/backtester.js @@ -56,7 +56,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.2'); +console.log('Starting BitBot Back-Tester v0.7.3'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -86,6 +86,7 @@ var createOrder = function(type, stopLoss) { logger.log('Placed buy order ' + BTCBalance + ' @ ' + lastClose); pricemon.setPosition('bought', lastClose); + advisor.setPosition({pos: 'bought', price: lastClose}); } else if(type === 'sell' && BTCBalance !== 0) { @@ -120,6 +121,7 @@ var createOrder = function(type, stopLoss) { logger.log('Placed sell order ' + usableBalance + ' @ ' + lastClose); pricemon.setPosition('sold', lastClose); + advisor.setPosition({pos: 'sold', price: lastClose}); } else { diff --git a/indicators/MACD.js b/indicators/MACD.js index 2a14cc7..46d37a1 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -4,12 +4,13 @@ var BigNumber = require('bignumber.js'); var indicator = function(options) { this.options = options; + this.position = {}; this.indicator = {}; this.previousIndicator = {}; this.advice = 'hold'; this.length = 0; - _.bindAll(this, 'calculate'); + _.bindAll(this, 'calculate', 'setPosition'); // indicatorOptions // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} @@ -73,4 +74,10 @@ indicator.prototype.calculate = function(cs) { }; +indicator.prototype.setPosition = function(pos) { + + this.position = pos; + +}; + module.exports = indicator; diff --git a/indicators/PPO.js b/indicators/PPO.js index 494644f..95d93b4 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -4,12 +4,13 @@ var BigNumber = require('bignumber.js'); var indicator = function(options) { this.options = options; + this.position = {}; this.indicator = {}; this.previousIndicator = {}; this.advice = 'hold'; this.length = 0; - _.bindAll(this, 'calculate'); + _.bindAll(this, 'calculate', 'setPosition'); // indicatorOptions // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} @@ -73,4 +74,10 @@ indicator.prototype.calculate = function(cs) { }; +indicator.prototype.setPosition = function(pos) { + + this.position = pos; + +}; + module.exports = indicator; diff --git a/indicators/template b/indicators/template index 790294c..ea8112c 100644 --- a/indicators/template +++ b/indicators/template @@ -4,8 +4,9 @@ var BigNumber = require('bignumber.js'); var indicator = function(options) { this.options = options; + this.position = {}; - _.bindAll(this, 'calculate'); + _.bindAll(this, 'calculate', 'setPosition'); // indicatorOptions // options: {The options required for your indicator to work} @@ -30,4 +31,14 @@ indicator.prototype.calculate = function(cs) { }; +indicator.prototype.setPosition = function(pos) { + + // This function is required and shouldn't be changed unless you know what you are doing. + // Provides the indicator with information qbout the current position. + // {pos: position, price: price} + + this.position = pos; + +}; + module.exports = indicator; diff --git a/package.json b/package.json index b2036b7..a63735a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.2", + "version": "0.7.3", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index 682ccd3..370791a 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -18,7 +18,7 @@ var advisor = function(indicatorSettings, candleStickSize, backTesting) { process.exit(); } - _.bindAll(this, 'start', 'update'); + _.bindAll(this, 'start', 'update', 'setPosition'); }; @@ -60,4 +60,10 @@ advisor.prototype.update = function(cs) { }; +advisor.prototype.setPosition = function(pos) { + + this.selectedIndicator.setPosition(pos); + +}; + module.exports = advisor; diff --git a/tests/dbhealth.js b/tests/dbhealth.js index 2ef4e5b..2fe0018 100644 --- a/tests/dbhealth.js +++ b/tests/dbhealth.js @@ -11,12 +11,18 @@ processor.on('initialized', function(){ var testPeriod = _.first(loopArray).period - 60; var success = true; + var previousCS; + _.each(loopArray, function(cs) { if(cs.period !== testPeriod + 60) { + console.log('There is a gap between the following two candlesticks:'); + console.log('Previous: ' + JSON.stringify(previousCS)); + console.log('Current: ' + JSON.stringify(cs)); success = false; } + previousCS = cs; testPeriod += 60; }); @@ -24,7 +30,7 @@ processor.on('initialized', function(){ if(success) { console.log("Database OK!"); } else { - console.log("Database corrupt, empty your database and try collection historical information again"); + console.log("Database corrupt/incomplete, empty your database and try collecting historical information again"); } }); From 7ed6cf828dac52126a5e3f4be19979acf8edc10f Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 8 Aug 2014 13:52:35 +0200 Subject: [PATCH 07/57] Fixed mistake in documentation --- README.md | 2 +- indicators/template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e4d792..6e84dff 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ var BigNumber = require('bignumber.js'); indicator.prototype.setPosition = function(pos) { // This function is required and shouldn't be changed unless you know what you are doing. - // Provides the indicator with information qbout the current position. + // Provides the indicator with information about the current position. // {pos: position, price: price} this.position = pos; diff --git a/indicators/template b/indicators/template index ea8112c..ac2aa11 100644 --- a/indicators/template +++ b/indicators/template @@ -34,7 +34,7 @@ indicator.prototype.calculate = function(cs) { indicator.prototype.setPosition = function(pos) { // This function is required and shouldn't be changed unless you know what you are doing. - // Provides the indicator with information qbout the current position. + // Provides the indicator with information about the current position. // {pos: position, price: price} this.position = pos; From 5716dd844266bccf8160556a7695e470c2221368 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 8 Aug 2014 16:16:18 +0200 Subject: [PATCH 08/57] Changes to documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e84dff..8bd1e55 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ Please read up on the following articles to help you choose the settings that be As of version 0.7 BitBot now uses indicators as small plugins. You can create your own indicator by using the following template: -var _ = require('underscore'); -var BigNumber = require('bignumber.js'); + var _ = require('underscore'); + var BigNumber = require('bignumber.js'); var indicator = function(options) { From e82674b0a6bba9acb1e8ac4b49044143aea9c9c0 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 10 Aug 2014 23:34:21 +0200 Subject: [PATCH 09/57] v0.7.4 Fix CS open values, new PSAR indicator Fixed an error in the calculation of the open value of aggregated candles and introduced a new indicator: Parabolic SAR. --- app.js | 2 +- backtester.js | 25 +++++- indicators/PSAR.js | 175 ++++++++++++++++++++++++++++++++++++++ package.json | 2 +- services/candlestorage.js | 10 +++ 5 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 indicators/PSAR.js diff --git a/app.js b/app.js index 9d89efd..324bae7 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.3'); +console.log('Starting BitBot v0.7.4'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index 8ba688e..fe43615 100644 --- a/backtester.js +++ b/backtester.js @@ -34,6 +34,9 @@ var profitPercentage = 0; var bhProfit = 0; var bhProfitPercentage = 0; var transactionFee = 0; +var totalTradedVolume = 0; +var highestUSDValue = 0; +var lowestUSDValue = USDBalance; var totalFeeCosts = 0; var totalFeeCostsPercentage = 0; var transactions = 0; @@ -56,7 +59,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.3'); +console.log('Starting BitBot Back-Tester v0.7.4'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -71,11 +74,21 @@ var createOrder = function(type, stopLoss) { usableBalance = Number(BigNumber(USDBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance)).round(2)); + totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(USDBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).round(2))); BTCBalance = Number(BigNumber(BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(lastClose)).round(2))); USDBalance = 0; + var newUSDBalance = Number(BigNumber(BTCBalance).times(BigNumber(lastClose)).round(2)); + + if(newUSDBalance > highestUSDValue) { + highestUSDValue = newUSDBalance; + } else if(newUSDBalance < lowestUSDValue) { + lowestUSDValue = newUSDBalance; + } + transactions += 1; if(stopLoss) { @@ -92,11 +105,19 @@ var createOrder = function(type, stopLoss) { usableBalance = Number(BigNumber(BTCBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance).times(BigNumber(lastClose))).round(2)); + totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(BTCBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).times(lastClose).round(2))); USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastClose)).round(2))); BTCBalance = 0; + if(USDBalance > highestUSDValue) { + highestUSDValue = USDBalance; + } else if(USDBalance < lowestUSDValue) { + lowestUSDValue = USDBalance; + } + exitUSD = USDBalance; var tradeResult = Number(BigNumber(exitUSD).minus(BigNumber(entryUSD)).round(2)); @@ -201,6 +222,8 @@ processor.on('initialized', function(){ logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); logger.log('Buy and Hold Profit: ' + bhProfit + ' (' + bhProfitPercentage + '%)'); logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); + logger.log('Total traded volue: ' + totalTradedVolume); + logger.log('Highest - Lowest USD Balance: ' + highestUSDValue + ' - ' + lowestUSDValue); logger.log('Open Price: ' + _.first(loopArray).open); logger.log('Close Price: ' + _.last(loopArray).close); logger.log('Start - End Date: ' + startDate + ' - ' + endDate); diff --git a/indicators/PSAR.js b/indicators/PSAR.js new file mode 100644 index 0000000..d16830a --- /dev/null +++ b/indicators/PSAR.js @@ -0,0 +1,175 @@ +var _ = require('underscore'); +var BigNumber = require('bignumber.js'); + +var indicator = function(options) { + + this.options = options; + this.position = {}; + this.csArray = []; + this.firstCandleDone = false; + + _.bindAll(this, 'calculate', 'setPosition'); + + // indicatorOptions + // options: {AFIncrement: number, maximumAF: number} + +}; + +//-------------------------------------------------------------------------------HelperFunctions + +var calculatePSAR = function(previousPSAR, previousEP, previousAF, previousTrend, limit) { + + var PSAR; + var temp; + + if(previousTrend === 1) { + temp = Number(BigNumber(previousPSAR).plus(BigNumber(previousAF).times(BigNumber(previousEP).minus(BigNumber(previousPSAR)))).round(2)); + PSAR = _.min([temp, limit]); + } else if (previousTrend === -1) { + temp = Number(BigNumber(previousPSAR).minus(BigNumber(previousAF).times(BigNumber(previousPSAR).minus(BigNumber(previousEP)))).round(2)); + PSAR = _.max([temp,limit]); + } + + return PSAR; + +}; + +var calculateTrend = function(previousTrend, PSAR, cs) { + + var trend; + + if(PSAR < cs.low) { + trend = 1; + } else if (PSAR > cs.high) { + trend = -1; + } else { + if (previousTrend === 1) { trend = -1;} + else { trend = 1;} + } + + return trend; + +}; + +var calculateLimit = function(previousTrend, arr) { + + var limit; + + var lowArr = [arr[0].low, arr[1].low]; + var highArr = [arr[0].high, arr[1].high]; + + if(previousTrend === 1) { + limit = _.min(lowArr); + } else if (previousTrend === -1) { + limit = _.max(highArr); + } + + return limit; + +}; + +var calculateEP = function(previousEP, trend, cs) { + + var EP; + + if(trend === 1) { + EP = _.max([cs.high,previousEP]); + } else if (trend === -1) { + EP = _.min([cs.low,previousEP]); + } else { + EP = previousEP; + } + + return EP; + +}; + +var calculateAF = function(EP, previousEP, trend, previousTrend, previousAF, AFIncrement, maximumAF) { + + var AF; + + var EPChanged = false; + + if(EP !== previousEP) {EPChanged = true;} + + if(EPChanged && trend === previousTrend && previousAF < maximumAF) { + AF = Number(BigNumber(previousAF).plus(BigNumber(AFIncrement)).round(2)); + } else if (trend !== previousTrend) { + AF = AFIncrement; + } else { + AF = previousAF; + } + + return AF; + +}; + +//-------------------------------------------------------------------------------HelperFunctions + +indicator.prototype.calculate = function(cs) { + + var limit, PSAR, trend, EP, AF; + + this.csArray.push(cs); + if(this.csArray.length > 3) { + this.csArray.shift(); + } + + if(!this.firstCandleDone) { + this.previousPSAR = Number(BigNumber(cs.low).round(2)); + this.previousEP = Number(BigNumber(cs.high).round(2)); + this.previousAF = this.options.AFIncrement; + this.previousTrend = 1; + this.firstCandleDone = true; + return 'hold'; + } + + limit = calculateLimit(this.previousTrend, this.csArray); + PSAR = calculatePSAR(this.previousPSAR, this.previousEP, this.previousAF, this.previousTrend, limit); + trend = calculateTrend(this.previousTrend, PSAR, cs); + if (trend !== this.previousTrend) { + limit = calculateLimit(trend, this.csArray); + this.previousPSAR = this.previousEP; + if(trend === 1) { + this.previousEP = cs.high; + } else { + this.previousEP = cs.low; + } + this.previousAF = this.options.AFIncrement; + PSAR = calculatePSAR(this.previousPSAR, this.previousEP, this.previousAF, trend, limit); + } + EP = calculateEP(this.previousEP, trend, cs); + AF = calculateAF(EP, this.previousEP, trend, this.previousTrend, this.previousAF, this.options.AFIncrement, this.options.maximumAF); + + cs.psar = PSAR; + + var advice; + + if(trend !== this.previousTrend) { + if(trend === 1) { + advice = 'buy'; + } else if(trend === -1) { + advice = 'sell'; + } else { + advice = 'hold'; + } + } else { + advice = 'hold'; + } + + this.previousPSAR = PSAR; + this.previousEP = EP; + this.previousAF = AF; + this.previousTrend = trend; + + return advice; + +}; + +indicator.prototype.setPosition = function(pos) { + + this.position = pos; + +}; + +module.exports = indicator; diff --git a/package.json b/package.json index a63735a..f2cc066 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.3", + "version": "0.7.4", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/candlestorage.js b/services/candlestorage.js index 0c69e0f..d2092b2 100644 --- a/services/candlestorage.js +++ b/services/candlestorage.js @@ -325,6 +325,16 @@ storage.prototype.getAggregatedCandleSticks = function(candleStickSize) { },this); + var vrelevantSticks = _.filter(relevantSticks, function(candleStick) { + + return candleStick.volume > 0; + + },this); + + if(vrelevantSticks.length > 0) { + relevantSticks = vrelevantSticks; + } + currentCandleStick.open = relevantSticks[0].open; currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; From 9451365df7b3dff64971262be50d903e4c2c7b91 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 00:08:23 +0200 Subject: [PATCH 10/57] License copyright changed --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 1cf4920..fe027c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 5an1ty +Copyright (c) 2014 Ruben Callewaert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. From ebf359c46d0b75bed779a4243321be36fd660a8f Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 00:33:42 +0200 Subject: [PATCH 11/57] Fixed typo in config.sample.js --- config.sample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.sample.js b/config.sample.js index 242af43..def3147 100644 --- a/config.sample.js +++ b/config.sample.js @@ -51,7 +51,7 @@ config.orderKeepAliveMinutes = config.candleStickSizeMinutes / 10; //------------------------------IndicatorSettings config.indicatorSettings = { indicator: 'MACD', - // Choises: Any indicator from the indicators folder + // Choices: Any indicator from the indicators folder options: {neededPeriods: 26, longPeriods: 26, shortPeriods: 12, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0} // Options needed for your indicator (Look them up in the indicator's file) }; From 136185744e493ddfe66d3482079359084e34ea95 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 19:43:39 +0200 Subject: [PATCH 12/57] v0.7.5 MongoDB Storage and Backtester bugfixes --- app.js | 2 +- backtester.js | 4 ++-- package.json | 4 ++-- services/candlestorage.js | 29 +++++++++++++++-------------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app.js b/app.js index 324bae7..bf9e711 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.4'); +console.log('Starting BitBot v0.7.5'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index fe43615..2dbcbdc 100644 --- a/backtester.js +++ b/backtester.js @@ -59,7 +59,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.4'); +console.log('Starting BitBot Back-Tester v0.7.5'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -201,7 +201,7 @@ processor.on('initialized', function(){ profit = Number(BigNumber(totalBalanceInUSD).minus(BigNumber(initialBalance)).round(2)); profitPercentage = Number(BigNumber(profit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); totalFeeCostsPercentage = Number(BigNumber(totalFeeCosts).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); - bhProfit = Number(BigNumber(_.last(loopArray).close).minus(_.first(loopArray).open).round(2)); + bhProfit = Number(BigNumber(_.last(loopArray).close).minus(_.first(loopArray).open).times(BigNumber(intialBalanceBTC)).round(2)); bhProfitPercentage = Number(BigNumber(bhProfit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); startDate = moment(new Date(_.first(loopArray).period*1000)).format('DD-MM-YYYY HH:mm:ss'); diff --git a/package.json b/package.json index f2cc066..0b8f8c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.4", + "version": "0.7.5", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { @@ -9,7 +9,7 @@ "bitstamp": "0.1.5", "mime": "1.2.11", "moment": "2.4.0", - "mongojs": "0.13.0", + "mongojs": "0.13.1", "pushover-notifications": "0.1.5", "underscore": "1.5.2", "ws": "0.4.31", diff --git a/services/candlestorage.js b/services/candlestorage.js index d2092b2..cfb0b1d 100644 --- a/services/candlestorage.js +++ b/services/candlestorage.js @@ -3,6 +3,7 @@ var logger = require('./loggingservice.js'); var BigNumber = require('bignumber.js'); var tools = require('./tools.js'); var db = require('mongojs'); +var async = require('async'); //------------------------------Config var config = require('../config.js'); @@ -367,7 +368,7 @@ storage.prototype.materialise = function(callback) { csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { - var filterPeriod = 0 + var filterPeriod = 0; if(!err && sticks.length > 0) { @@ -383,31 +384,31 @@ storage.prototype.materialise = function(callback) { if(materialiseCs.length > 0) { - csCollection.remove({ period: { $gte: filterPeriod } }, function(err, resp) { + async.eachSeries(materialiseCs, function(cs, cb) { - if(err) { + csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, doc) { - csDatastore.close(); + if(err) { - callback(err); + cb(err); - } else { + } else { - csCollection.insert(materialiseCs, function(err) { + cb(); - csDatastore.close(); + } - if(err) { + }); - callback(err); + }, function(err) { - } else { + if(err) { - callback(null); + callback(err); - } + } else { - }); + callback(null); } From e4f832b67698ed965b16f0b3e089562116d8b99c Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 21:43:41 +0200 Subject: [PATCH 13/57] Extra API Error Logging --- services/api.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/services/api.js b/services/api.js index 46f437a..744f0c3 100644 --- a/services/api.js +++ b/services/api.js @@ -54,11 +54,11 @@ api.prototype.retry = function(method, args) { // run the failed method again with the same // arguments after wait - setTimeout(function() { method.apply(self, args) }, 1000*15); + setTimeout(function() { method.apply(self, args); }, 1000*15); }; -api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, cb) { +api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, caller, cb) { var args = _.toArray(receivedArgs); @@ -73,13 +73,13 @@ api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, cb) { } else if(retryAllowed) { - logger.error('Couldn\'t connect to the API, retrying in 15 seconds!'); + logger.error(caller + ' Couldn\'t connect to the API, retrying in 15 seconds!'); logger.error(JSON.stringify(err).substring(0,99)); return this.retry(method, args); } else { - logger.error('Couldn\'t connect to the API.'); + logger.error(caller + ' Couldn\'t connect to the API.'); //cb(err, result); return logger.error(JSON.stringify(err).substring(0,99)); @@ -128,7 +128,7 @@ api.prototype.getTrades = function(cb) { }; - this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, false, handler)); + this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, false, 'getTrades', handler)); } else if(this.exchange === 'kraken') { @@ -150,7 +150,7 @@ api.prototype.getTrades = function(cb) { }; - this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, false, handler)); + this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, false, 'getTrades', handler)); } @@ -181,7 +181,7 @@ api.prototype.getBalance = function(cb) { }; - this.bitstamp.balance(this.errorHandler(this.getBalance, args, true, handler)); + this.bitstamp.balance(this.errorHandler(this.getBalance, args, true, 'getBalance', handler)); } else if(this.exchange === 'kraken') { @@ -203,7 +203,7 @@ api.prototype.getBalance = function(cb) { currencyValue = 0; } - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, true, function(err, data) { + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, true, 'getBalance', function(err, data) { var fee = parseFloat(_.find(data.result.fees, function(value, key) { return key === pair; @@ -215,7 +215,7 @@ api.prototype.getBalance = function(cb) { }.bind(this); - this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, true, handler)); + this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, true, 'getBalance', handler)); } @@ -251,7 +251,7 @@ api.prototype.getOrderBook = function(cb) { }; - this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, true, handler)); + this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, true, 'getOrderBook', handler)); } else if(this.exchange === 'kraken') { @@ -275,7 +275,7 @@ api.prototype.getOrderBook = function(cb) { }; - this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, true, handler)); + this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, true, 'getOrderBook', handler)); } @@ -305,11 +305,11 @@ api.prototype.placeOrder = function(type, amount, price, cb) { if(type === 'buy') { - this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, true, handler)); + this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); } else if (type === 'sell') { - this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, true, handler)); + this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); } else { @@ -326,11 +326,11 @@ api.prototype.placeOrder = function(type, amount, price, cb) { if(type === 'buy') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); } else if (type === 'sell') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); } else { @@ -376,7 +376,7 @@ api.prototype.orderFilled = function(order, cb) { }; - this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, false, handler)); + this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, false, 'orderFilled', handler)); } else if(this.exchange === 'kraken') { @@ -400,7 +400,7 @@ api.prototype.orderFilled = function(order, cb) { }; - this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, false, handler)); + this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, false, 'orderFilled', handler)); } @@ -430,7 +430,7 @@ api.prototype.cancelOrder = function(order, cb) { }; - this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, true, handler)); + this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, true, 'cancelOrder', handler)); } else if(this.exchange === 'kraken') { @@ -448,7 +448,7 @@ api.prototype.cancelOrder = function(order, cb) { }; - this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, true, handler)); + this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, true, 'cancelOrder', handler)); } else { From ee41e43cd480787a3bdb3814ebd3c84a21258bef Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 21:58:10 +0200 Subject: [PATCH 14/57] Stop bot after 30 consecutive failed API calls --- services/dataretriever.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/dataretriever.js b/services/dataretriever.js index e735c50..daa5143 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -6,6 +6,7 @@ var api = require('./api.js'); var downloader = function(refreshInterval){ this.refreshInterval = refreshInterval; + this.noTradesCount = 0; _.bindAll(this, 'start', 'stop', 'processTrades'); @@ -21,6 +22,17 @@ downloader.prototype.processTrades = function(err, trades) { if(!err) { + if(trades.length === 0) { + this.noTradesCount += 1; + } else { + this.noTradesCount = 0; + } + + if(this.noTradesCount >= 30) { + logger.error('Haven\'t received data from the API for 30 consecutive attempts, stopping qpplication'); + return process.exit(); + } + this.emit('update', trades); } From 56a4005b4fe7cf49377928ae83f27bc57fcb5dc2 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 22:54:40 +0200 Subject: [PATCH 15/57] Bugfix to API and Dataretriever --- services/api.js | 2 +- services/dataretriever.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/services/api.js b/services/api.js index 744f0c3..6d4f3ec 100644 --- a/services/api.js +++ b/services/api.js @@ -80,7 +80,7 @@ api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, caller } else { logger.error(caller + ' Couldn\'t connect to the API.'); - //cb(err, result); + cb(err, null); return logger.error(JSON.stringify(err).substring(0,99)); } diff --git a/services/dataretriever.js b/services/dataretriever.js index daa5143..8768587 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -22,19 +22,19 @@ downloader.prototype.processTrades = function(err, trades) { if(!err) { - if(trades.length === 0) { - this.noTradesCount += 1; - } else { - this.noTradesCount = 0; - } + this.noTradesCount = 0; + + this.emit('update', trades); + + } else { + + this.noTradesCount += 1; if(this.noTradesCount >= 30) { logger.error('Haven\'t received data from the API for 30 consecutive attempts, stopping qpplication'); return process.exit(); } - - this.emit('update', trades); - + } }; From 449e2effa563763a6574dfb6fcd4a8a2a9475c75 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 23:04:10 +0200 Subject: [PATCH 16/57] Fixed typo in dataretriever.js --- services/dataretriever.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/dataretriever.js b/services/dataretriever.js index 8768587..80658ec 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -31,10 +31,10 @@ downloader.prototype.processTrades = function(err, trades) { this.noTradesCount += 1; if(this.noTradesCount >= 30) { - logger.error('Haven\'t received data from the API for 30 consecutive attempts, stopping qpplication'); + logger.error('Haven\'t received data from the API for 30 consecutive attempts, stopping application'); return process.exit(); } - + } }; From 6c6fc095291a8c5eaadb8f92187e3582579db480 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 11 Aug 2014 23:22:22 +0200 Subject: [PATCH 17/57] Fixed another bug in api.js --- services/api.js | 80 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/services/api.js b/services/api.js index 6d4f3ec..22ec34a 100644 --- a/services/api.js +++ b/services/api.js @@ -116,15 +116,23 @@ api.prototype.getTrades = function(cb) { handler = function(err, response) { - var trades = _.map(response, function(t) { + if(!err) { - return {date: parseInt(t.date), price: parseFloat(t.price), amount: parseFloat(t.amount)}; + var trades = _.map(response, function(t) { - }); + return {date: parseInt(t.date), price: parseFloat(t.price), amount: parseFloat(t.amount)}; + + }); + + var result = _.sortBy(trades, function(trade){ return trade.date; }); + + cb(null, result); - var result = _.sortBy(trades, function(trade){ return trade.date; }); + } else { + + cb(err, null); - cb(null, result); + } }; @@ -134,19 +142,27 @@ api.prototype.getTrades = function(cb) { handler = function(err, data) { - var values = _.find(data.result, function(value, key) { + if(!err) { - return key === pair; + var values = _.find(data.result, function(value, key) { - }); + return key === pair; - var trades = _.map(values, function(t) { + }); - return {date: parseInt(t[2]), price: parseFloat(t[0]), amount: parseFloat(t[1])}; + var trades = _.map(values, function(t) { - }); + return {date: parseInt(t[2]), price: parseFloat(t[0]), amount: parseFloat(t[1])}; + + }); + + cb(null, trades); + + } else { - cb(null, trades); + cb(err, null); + + } }; @@ -358,19 +374,27 @@ api.prototype.orderFilled = function(order, cb) { handler = function(err, result) { - var open = _.find(result, function(o) { + if(!err) { - return o.id === order; + var open = _.find(result, function(o) { - }, this); + return o.id === order; - if(open) { + }, this); - cb(null, false); + if(open) { + + cb(null, false); + + } else { + + cb(null, true); + + } } else { - cb(null, true); + cb(err, null); } @@ -382,19 +406,27 @@ api.prototype.orderFilled = function(order, cb) { handler = function(err, data) { - var open = _.find(data.result.open, function(value, key) { + if(!err) { - return key === order; + var open = _.find(data.result.open, function(value, key) { - }); + return key === order; - if(open) { + }); - cb(null, false); + if(open) { + + cb(null, false); + + } else { + + cb(null, true); + + } } else { - cb(null, true); + cb(err, null); } From f4e2ed1c27d15ef5deb2fcf9c694ebd544b2258b Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 12 Aug 2014 20:42:47 +0200 Subject: [PATCH 18/57] Close DB connections properly in candlestorage.js --- services/candlestorage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/candlestorage.js b/services/candlestorage.js index cfb0b1d..2144486 100644 --- a/services/candlestorage.js +++ b/services/candlestorage.js @@ -402,6 +402,8 @@ storage.prototype.materialise = function(callback) { }, function(err) { + csDatastore.close(); + if(err) { callback(err); From 2b8f6288ba9672bd20604c371e473f99e00e5890 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 12 Aug 2014 23:20:34 +0200 Subject: [PATCH 19/57] v0.7.6 Fixed bugs in API and Candlestorage modules --- app.js | 2 +- backtester.js | 2 +- package.json | 2 +- services/api.js | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index bf9e711..9d40880 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.5'); +console.log('Starting BitBot v0.7.6'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index 2dbcbdc..4140896 100644 --- a/backtester.js +++ b/backtester.js @@ -59,7 +59,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.5'); +console.log('Starting BitBot Back-Tester v0.7.6'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/package.json b/package.json index 0b8f8c1..591a295 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.5", + "version": "0.7.6", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/api.js b/services/api.js index 22ec34a..eca966d 100644 --- a/services/api.js +++ b/services/api.js @@ -66,7 +66,15 @@ api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, caller if(err) { - if(this.exchange === 'kraken' && err[0] === 'EQuery:Unknown asset pair') { + var parsedError; + + if(JSON.stringify(err) === '{}' && err.message) { + parsedError = err.message; + } else { + parsedError = JSON.stringify(err); + } + + if(this.exchange === 'kraken' && parsedError === '["EQuery:Unknown asset pair"]') { logger.error('Kraken returned Unknown asset pair error, exiting!'); return process.exit(); @@ -74,14 +82,14 @@ api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, caller } else if(retryAllowed) { logger.error(caller + ' Couldn\'t connect to the API, retrying in 15 seconds!'); - logger.error(JSON.stringify(err).substring(0,99)); + logger.error(parsedError.substring(0,99)); return this.retry(method, args); } else { logger.error(caller + ' Couldn\'t connect to the API.'); - cb(err, null); - return logger.error(JSON.stringify(err).substring(0,99)); + cb(parsedError, null); + return logger.error(parsedError.substring(0,99)); } From 0ecdb082ba4058a8748ec556b722316881ac5fad Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 17 Aug 2014 19:44:20 +0200 Subject: [PATCH 20/57] v0.7.7 Moved DB functions, Enhanced profit report Moved DB functions to a separate module and enhanced profit calculation to persist through application restarts. --- README.md | 5 + app.js | 4 +- backtester.js | 2 +- config.sample.js | 2 + package.json | 2 +- services/candlestorage.js | 120 ++---------------------- services/db.js | 183 +++++++++++++++++++++++++++++++++++++ services/profitreporter.js | 76 ++++++++++++++- 8 files changed, 272 insertions(+), 122 deletions(-) create mode 100644 services/db.js diff --git a/README.md b/README.md index 8bd1e55..02e00d1 100644 --- a/README.md +++ b/README.md @@ -136,3 +136,8 @@ Remember the backtester simulates your trading strategy on the data you collecte The provided trading algorithms are well known and documented on the internet (MADC, PPO). I do not guarantee you will make any profit when using this bot... For better results, consider writing your own algorithm and share it with the community in a pull request :-). + +# Donations + +If you enjoyed using BitBot or would like to help me continue development of this bot, consider buying me a beer: +(BTC) 1BitBotSYYMAsn6c1AsrxWphWAGkNE6hmQ diff --git a/app.js b/app.js index 9d40880..6753abe 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.6'); +console.log('Starting BitBot v0.7.7'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); @@ -48,7 +48,7 @@ processor.on('initialized', function(){ processor.on('initialDBWrite', function(){ - reporter.updateBalance(false); + reporter.start(config.resetInitialBalances); advisor.start(); diff --git a/backtester.js b/backtester.js index 4140896..ed396e4 100644 --- a/backtester.js +++ b/backtester.js @@ -59,7 +59,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.6'); +console.log('Starting BitBot Back-Tester v0.7.7'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/config.sample.js b/config.sample.js index def3147..b72af3a 100644 --- a/config.sample.js +++ b/config.sample.js @@ -33,6 +33,8 @@ config.apiSettings = { config.mongoConnectionString = 'localhost/bitbot'; // The connection string for your MongoDB Installation // Example: config.mongoConnectionString = 'username:password@example.com/mydb'; +config.resetInitialBalances = false; +// Set this to true if you want to reset the initialBalance stored by the bot //------------------------------dbSettings //------------------------------downloaderSettings diff --git a/package.json b/package.json index 591a295..b47c282 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.6", + "version": "0.7.7", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/candlestorage.js b/services/candlestorage.js index 2144486..43ddbe5 100644 --- a/services/candlestorage.js +++ b/services/candlestorage.js @@ -1,21 +1,13 @@ var _ = require('underscore'); -var logger = require('./loggingservice.js'); var BigNumber = require('bignumber.js'); var tools = require('./tools.js'); -var db = require('mongojs'); -var async = require('async'); - -//------------------------------Config -var config = require('../config.js'); -//------------------------------Config +var db = require('./db.js'); var storage = function() { - this.dbCollectionName = config.exchangeSettings.exchange + config.exchangeSettings.currencyPair.pair; - this.candleSticksCollection = []; - _.bindAll(this, 'selectCollection', 'set', 'push', 'removeOldCandles', 'flush', 'getAllCandlesSince', 'getLastNCandles', 'getLastPeriod', 'getLastNonEmptyPeriod', 'getLastClose', 'getLastNonEmptyClose', 'getCandle', 'length', 'getAverageCandleStickSize', 'generateWebServerArray', 'getFinishedAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getAggregatedCandleSticks', 'materialise', 'removeOldDBCandles', 'getDBCandles'); + _.bindAll(this, 'selectCollection', 'set', 'push', 'removeOldCandles', 'flush', 'getAllCandlesSince', 'getLastNCandles', 'getLastPeriod', 'getLastNonEmptyPeriod', 'getLastClose', 'getLastNonEmptyClose', 'getCandle', 'length', 'getAverageCandleStickSize', 'generateWebServerArray', 'getFinishedAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getAggregatedCandleSticks', 'materialise', 'getDBCandles'); }; @@ -104,7 +96,7 @@ storage.prototype.removeOldCandles = function() { }); if(candleStickSize === 1) { - this.removeOldDBCandles(oldPeriod); + db.removeOldDBCandles(oldPeriod); } }, this); @@ -204,7 +196,6 @@ storage.prototype.getLastClose = function(candleStickSize) { }; - storage.prototype.getLastNonEmptyClose = function(candleStickSize) { var candleStickArray = this.selectCollection(candleStickSize); @@ -360,118 +351,21 @@ storage.prototype.getAggregatedCandleSticks = function(candleStickSize) { }; storage.prototype.materialise = function(callback) { - var candleStickArray = this.selectCollection(1); - - var csDatastore = db(config.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); - - csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { - - var filterPeriod = 0; - - if(!err && sticks.length > 0) { - - filterPeriod = sticks[0].period; - - } - - materialiseCs = _.filter(candleStickArray.candleSticks, function(cs){ - - return cs.period >= filterPeriod; - - }); - - if(materialiseCs.length > 0) { - - async.eachSeries(materialiseCs, function(cs, cb) { - - csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, doc) { - - if(err) { - - cb(err); - - } else { - - cb(); - - } - - }); - - }, function(err) { - - csDatastore.close(); - - if(err) { - - callback(err); - - } else { - - callback(null); - - } - - }); - - } else { - - callback(null); - - } - - }); - -}; - -storage.prototype.removeOldDBCandles = function(filterPeriod) { - - var csDatastore = db(config.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); - - csCollection.remove({ period: { $lte: filterPeriod } }, function(err, resp) { - - csDatastore.close(); - - }); - + db.materialise(candleStickArray.candleSticks, callback); }; storage.prototype.getDBCandles = function(callback) { + db.getDBCandles(function(err, storageCandleSticks) { - var csDatastore = db(config.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); - - csCollection.ensureIndex({period: 1}); - - csCollection.find({}).sort({period:1}, function(err, candleSticks) { - - csDatastore.close(); - - if(err) { - - callback(err); - - } else if(candleSticks.length > 0 ){ - - var storageCandleSticks = _.map(candleSticks, function(candleStick){ - return {'period':candleStick.period, 'open':candleStick.open, 'high':candleStick.high, 'low':candleStick.low, 'close':candleStick.close, 'volume':candleStick.volume, 'vwap':candleStick.vwap}; - }); - + if(!err && storageCandleSticks) { this.set(storageCandleSticks); - callback(null); - } else { - - callback(null); - + callback(err); } }.bind(this)); - }; var candlestorage = new storage(); diff --git a/services/db.js b/services/db.js new file mode 100644 index 0000000..9725ed5 --- /dev/null +++ b/services/db.js @@ -0,0 +1,183 @@ +var _ = require('underscore'); +var mongo = require('mongojs'); +var async = require('async'); + +//------------------------------Config +var config = require('../config.js'); +//------------------------------Config + +var db = function() { + + this.pair = config.exchangeSettings.currencyPair.pair; + this.dbCollectionName = config.exchangeSettings.exchange + config.exchangeSettings.currencyPair.pair; + + _.bindAll(this, 'materialise', 'removeOldDBCandles', 'getDBCandles', 'getInitialBalance', 'setInitialBalance'); + +}; + +db.prototype.materialise = function(candleStickArray, callback) { + + var csDatastore = mongo(config.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { + + var filterPeriod = 0; + + if(!err && sticks.length > 0) { + + filterPeriod = sticks[0].period; + + } + + materialiseCs = _.filter(candleStickArray, function(cs){ + + return cs.period >= filterPeriod; + + }); + + if(materialiseCs.length > 0) { + + async.eachSeries(materialiseCs, function(cs, cb) { + + csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, doc) { + + if(err) { + + cb(err); + + } else { + + cb(); + + } + + }); + + }, function(err) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else { + + callback(null); + + } + + }); + + } else { + + callback(null); + + } + + }); + +}; + +db.prototype.removeOldDBCandles = function(filterPeriod) { + + var csDatastore = mongo(config.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.remove({ period: { $lte: filterPeriod } }, function(err, resp) { + + csDatastore.close(); + + }); + +}; + +db.prototype.getDBCandles = function(callback) { + + var csDatastore = mongo(config.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.ensureIndex({period: 1}); + + csCollection.find({}).sort({period:1}, function(err, candleSticks) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else if(candleSticks.length > 0 ){ + + var storageCandleSticks = _.map(candleSticks, function(candleStick){ + return {'period':candleStick.period, 'open':candleStick.open, 'high':candleStick.high, 'low':candleStick.low, 'close':candleStick.close, 'volume':candleStick.volume, 'vwap':candleStick.vwap}; + }); + + callback(null, storageCandleSticks); + + } else { + + callback(null); + + } + + }.bind(this)); + +}; + +db.prototype.getInitialBalance = function(callback) { + + var csDatastore = mongo(config.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); + + csCollection.find({pair: this.pair}).limit(1, function(err, balance) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else if(balance.length > 0 ){ + + var initialBalance = balance[0].initialBalance; + + callback(null, initialBalance); + + } else { + + callback(null, null); + + } + + }.bind(this)); + +}; + +db.prototype.setInitialBalance = function(initialBalance, callback) { + + var csDatastore = mongo(config.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); + + csCollection.update({pair: this.pair}, {pair: this.pair, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else { + + callback(null); + + } + + }.bind(this)); + +}; + +var database = new db(); + +module.exports = database; diff --git a/services/profitreporter.js b/services/profitreporter.js index 2ce0581..9278edc 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -2,13 +2,14 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); var async = require('async'); var logger = require('./loggingservice.js'); +var db = require('./db.js'); var api = require('./api.js'); var profitreporter = function(currencyPair) { this.currencyPair = currencyPair; - _.bindAll(this, 'createReport', 'processBalance', 'updateBalance'); + _.bindAll(this, 'intialize', 'createReport', 'processBalance', 'start', 'updateBalance'); }; @@ -18,6 +19,38 @@ var EventEmitter = require('events').EventEmitter; Util.inherits(profitreporter, EventEmitter); //---EventEmitter Setup +profitreporter.prototype.intialize = function(err, result) { + + this.currencyBalance = parseFloat(result.balance.currencyAvailable); + this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); + + this.highestBid = _.first(result.orderBook.bids).currencyPrice; + this.assetBalanceInCurrency = BigNumber(this.assetBalance).times(BigNumber(this.highestBid)); + + this.initalTotalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); + + db.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { + + if(err) { + + logger.error('Couldn\'t get initialBalance due to a database error'); + logger.error(err.stack); + + process.exit(); + + } else { + + if(this.resetInitialBalances) { + logger.log(this.currencyPair.pair + ' Balance reset successfully, change the configuration setting back to false and restart the application.'); + process.exit(); + } + + } + + }.bind(this)); + +}; + profitreporter.prototype.createReport = function() { var report = this.currencyPair.asset + ': ' + this.assetBalance + ' ' + this.currencyPair.currency + ': ' + this.currencyBalance + ' Total in ' + this.currencyPair.currency + ': ' + this.totalCurrencyBalance + ' Profit: ' + this.profitAbsolute + ' (' + this.profitPercentage + '%)'; @@ -36,10 +69,6 @@ profitreporter.prototype.processBalance = function(err, result) { this.highestBid = _.first(result.orderBook.bids).currencyPrice; this.assetBalanceInCurrency = BigNumber(this.assetBalance).times(BigNumber(this.highestBid)); - if(!this.totalCurrencyBalance) { - this.initalTotalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); - } - this.totalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); this.profitAbsolute = Number(BigNumber(this.totalCurrencyBalance).minus(this.initalTotalCurrencyBalance)); this.profitPercentage = Number(BigNumber(this.profitAbsolute).dividedBy(BigNumber(this.initalTotalCurrencyBalance)).times(BigNumber(100)).round(2)); @@ -52,6 +81,43 @@ profitreporter.prototype.processBalance = function(err, result) { }; +profitreporter.prototype.start = function(resetInitialBalances) { + + this.resetInitialBalances = resetInitialBalances; + + db.getInitialBalance(function(err, result) { + + if(err) { + + logger.error('Couldn\'t get initialBalance due to a database error'); + logger.error(err.stack); + + process.exit(); + + } else { + + if(result && !resetInitialBalances) { + + this.initalTotalCurrencyBalance = result; + + } else { + + async.series( + { + balance: api.getBalance, + orderBook: api.getOrderBook + }, + this.intialize + ); + + } + + } + + }.bind(this)); + +}; + profitreporter.prototype.updateBalance = function(includeReport) { this.includeReport = includeReport; From 522c5dac7f27d5945bc4d6801a817816a6baa2ec Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 19 Aug 2014 21:52:32 +0200 Subject: [PATCH 21/57] v0.7.8 Fixed a bug in the Kraken API wrapper Restructured the API wrapper and fixed a bug with order monitoring on Kraken. --- app.js | 2 +- backtester.js | 4 +- package.json | 2 +- services/api.js | 204 +++++++++++++++++++++++++------------ services/dataretriever.js | 4 +- services/ordermonitor.js | 14 +-- services/profitreporter.js | 8 +- services/tradingagent.js | 2 +- 8 files changed, 157 insertions(+), 83 deletions(-) diff --git a/app.js b/app.js index 6753abe..b93d554 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.7'); +console.log('Starting BitBot v0.7.8'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index ed396e4..9866d3e 100644 --- a/backtester.js +++ b/backtester.js @@ -59,7 +59,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.7'); +console.log('Starting BitBot Back-Tester v0.7.8'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -154,7 +154,7 @@ var createOrder = function(type, stopLoss) { processor.on('initialized', function(){ - api.getBalance(function(err, result){ + api.getBalance(true, function(err, result){ transactionFee = result.fee; diff --git a/package.json b/package.json index b47c282..544e5cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.7", + "version": "0.7.8", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/api.js b/services/api.js index eca966d..ac81b30 100644 --- a/services/api.js +++ b/services/api.js @@ -110,7 +110,7 @@ api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, caller }; -api.prototype.getTrades = function(cb) { +api.prototype.getTrades = function(retry, cb) { var args = arguments; @@ -144,7 +144,7 @@ api.prototype.getTrades = function(cb) { }; - this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, false, 'getTrades', handler)); + this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); } else if(this.exchange === 'kraken') { @@ -174,7 +174,7 @@ api.prototype.getTrades = function(cb) { }; - this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, false, 'getTrades', handler)); + this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); } @@ -184,7 +184,7 @@ api.prototype.getTrades = function(cb) { }; -api.prototype.getBalance = function(cb) { +api.prototype.getBalance = function(retry, cb) { var args = arguments; @@ -201,45 +201,69 @@ api.prototype.getBalance = function(cb) { handler = function(err, result) { - cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); + if(!err) { + + cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); + + } else { + + cb(err, null); + + } }; - this.bitstamp.balance(this.errorHandler(this.getBalance, args, true, 'getBalance', handler)); + this.bitstamp.balance(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); } else if(this.exchange === 'kraken') { handler = function(err, data) { - var assetValue = _.find(data.result, function(value, key) { - return key === asset; - }); + if(!err) { - var currencyValue = _.find(data.result, function(value, key) { - return key === currency; - }); + var assetValue = _.find(data.result, function(value, key) { + return key === asset; + }); - if(!assetValue) { - assetValue = 0; - } + var currencyValue = _.find(data.result, function(value, key) { + return key === currency; + }); - if(!currencyValue) { - currencyValue = 0; - } + if(!assetValue) { + assetValue = 0; + } - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, true, 'getBalance', function(err, data) { + if(!currencyValue) { + currencyValue = 0; + } - var fee = parseFloat(_.find(data.result.fees, function(value, key) { - return key === pair; - }).fee); + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, retry, 'getBalance', function(err, data) { - cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + if(!err) { - })); + var fee = parseFloat(_.find(data.result.fees, function(value, key) { + return key === pair; + }).fee); + + cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + + } else { + + cb(err, null); + + } + + })); + + } else { + + cb(err, null); + + } }.bind(this); - this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, true, 'getBalance', handler)); + this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); } @@ -249,7 +273,7 @@ api.prototype.getBalance = function(cb) { }; -api.prototype.getOrderBook = function(cb) { +api.prototype.getOrderBook = function(retry, cb) { var args = arguments; @@ -263,43 +287,59 @@ api.prototype.getOrderBook = function(cb) { handler = function(err, result) { - var bids = _.map(result.bids, function(bid) { - return {assetAmount: bid[1], currencyPrice: bid[0]}; - }); + if(!err) { + + var bids = _.map(result.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); + + var asks = _.map(result.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); + + cb(null, {bids: bids, asks: asks}); + + } else { - var asks = _.map(result.asks, function(ask) { - return {assetAmount: ask[1], currencyPrice: ask[0]}; - }); + cb(err, null); - cb(null, {bids: bids, asks: asks}); + } }; - this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, true, 'getOrderBook', handler)); + this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); } else if(this.exchange === 'kraken') { handler = function(err, data) { - var orderbook = _.find(data.result, function(value, key) { + if(!err) { + + var orderbook = _.find(data.result, function(value, key) { - return key === pair; + return key === pair; + + }); - }); + var bids = _.map(orderbook.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); - var bids = _.map(orderbook.bids, function(bid) { - return {assetAmount: bid[1], currencyPrice: bid[0]}; - }); + var asks = _.map(orderbook.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); - var asks = _.map(orderbook.asks, function(ask) { - return {assetAmount: ask[1], currencyPrice: ask[0]}; - }); + cb(null, {bids: bids, asks: asks}); - cb(null, {bids: bids, asks: asks}); + } else { + + cb(err, null); + + } }; - this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, true, 'getOrderBook', handler)); + this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); } @@ -309,7 +349,7 @@ api.prototype.getOrderBook = function(cb) { }; -api.prototype.placeOrder = function(type, amount, price, cb) { +api.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; @@ -323,17 +363,25 @@ api.prototype.placeOrder = function(type, amount, price, cb) { handler = function(err, result) { - cb(null, {txid: result.id}); + if(!err) { + + cb(null, {txid: result.id}); + + } else { + + cb(err, null); + + } }; if(type === 'buy') { - this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); + this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else if (type === 'sell') { - this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); + this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else { @@ -344,17 +392,25 @@ api.prototype.placeOrder = function(type, amount, price, cb) { handler = function(err, data) { - cb(null, {txid: data.result.txid[0]}); + if(!err) { + + cb(null, {txid: data.result.txid[0]}); + + } else { + + cb(err, null); + + } }; if(type === 'buy') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else if (type === 'sell') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, true, 'placeOrder', handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else { @@ -370,7 +426,7 @@ api.prototype.placeOrder = function(type, amount, price, cb) { }; -api.prototype.orderFilled = function(order, cb) { +api.prototype.orderFilled = function(order, retry, cb) { var args = arguments; @@ -408,7 +464,7 @@ api.prototype.orderFilled = function(order, cb) { }; - this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, false, 'orderFilled', handler)); + this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); } else if(this.exchange === 'kraken') { @@ -440,7 +496,7 @@ api.prototype.orderFilled = function(order, cb) { }; - this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, false, 'orderFilled', handler)); + this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); } @@ -450,7 +506,7 @@ api.prototype.orderFilled = function(order, cb) { }; -api.prototype.cancelOrder = function(order, cb) { +api.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; @@ -462,33 +518,49 @@ api.prototype.cancelOrder = function(order, cb) { handler = function(err, result) { - if(!result.error) { - cb(null, true); + if(!err) { + + if(!result.error) { + cb(null, true); + } else { + cb(null, false); + } + } else { - cb(null, false); + + cb(err, null); + } }; - this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, true, 'cancelOrder', handler)); + this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); } else if(this.exchange === 'kraken') { - this.orderFilled(order, function(err, filled) { + this.orderFilled(order, true, function(err, filled) { if(!filled) { - handler = function(err, result) { + handler = function(err, data) { + + if(!err) { + + if(data.result.count > 0) { + cb(null, true); + } else { + cb(null, false); + } - if(result.count > 0) { - cb(null, true); } else { - cb(null, false); + + cb(err, null); + } }; - this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, true, 'cancelOrder', handler)); + this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); } else { diff --git a/services/dataretriever.js b/services/dataretriever.js index 80658ec..5c9714e 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -43,10 +43,10 @@ downloader.prototype.start = function() { logger.log('Downloader started!'); - api.getTrades(this.processTrades); + api.getTrades(false, this.processTrades); this.downloadInterval = setInterval(function(){ - api.getTrades(this.processTrades); + api.getTrades(false, this.processTrades); }.bind(this),1000 * this.refreshInterval); }; diff --git a/services/ordermonitor.js b/services/ordermonitor.js index c26d5d3..28e4e1a 100644 --- a/services/ordermonitor.js +++ b/services/ordermonitor.js @@ -5,7 +5,7 @@ var api = require('./api.js'); var ordermonitor = function() { - _.bindAll(this, 'checkCancellation', 'processCancellation', 'processSimulation', 'add', 'resolvePreviousOrder'); + _.bindAll(this, 'checkFilled', 'processCancellation', 'processSimulation', 'add', 'resolvePreviousOrder'); this.checkOrder = {}; @@ -17,7 +17,7 @@ var EventEmitter = require('events').EventEmitter; Util.inherits(ordermonitor, EventEmitter); //---EventEmitter Setup -ordermonitor.prototype.checkCancellation = function(checkOrder, filled) { +ordermonitor.prototype.checkFilled = function(checkOrder, filled) { if(checkOrder.status !== 'filled') { @@ -86,9 +86,9 @@ ordermonitor.prototype.add = function(orderDetails, cancelTime) { this.checkOrder.interval = setInterval(function() { - api.orderFilled(this.checkOrder.id, function(err, response){ + api.orderFilled(this.checkOrder.id, false, function(err, response){ if(!err) { - this.checkCancellation(this.checkOrder, response); + this.checkFilled(this.checkOrder, response); } }.bind(this)); @@ -100,7 +100,9 @@ ordermonitor.prototype.add = function(orderDetails, cancelTime) { if(this.checkOrder.status === 'open') { - api.cancelOrder(this.checkOrder.id, function(err, response) { + logger.log('Cancelling order: ' + this.checkOrder.id); + + api.cancelOrder(this.checkOrder.id, true, function(err, response) { this.processCancellation(this.checkOrder, response); }.bind(this)); @@ -119,7 +121,7 @@ ordermonitor.prototype.resolvePreviousOrder = function() { clearInterval(this.checkOrder.interval); clearTimeout(this.checkOrder.timeout); - this.checkOrder.status = 'cancelled'; + this.checkOrder.status = 'resolved'; } diff --git a/services/profitreporter.js b/services/profitreporter.js index 9278edc..73e2ef3 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -104,8 +104,8 @@ profitreporter.prototype.start = function(resetInitialBalances) { async.series( { - balance: api.getBalance, - orderBook: api.getOrderBook + balance: function(cb) {api.getBalance(true, cb);}, + orderBook: function(cb) {api.api.getOrderBook(true, cb);} }, this.intialize ); @@ -124,8 +124,8 @@ profitreporter.prototype.updateBalance = function(includeReport) { async.series( { - balance: api.getBalance, - orderBook: api.getOrderBook + balance: function(cb) {api.getBalance(true, cb);}, + orderBook: function(cb) {api.api.getOrderBook(true, cb);} }, this.processBalance ); diff --git a/services/tradingagent.js b/services/tradingagent.js index c55935a..7fa4f50 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -119,7 +119,7 @@ tradingagent.prototype.placeRealOrder = function() { } else { - api.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, this.processOrder); + api.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, true, this.processOrder); } From 6e4c1cd4d9122c194c0df85cd9bafa5bf21759e3 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 19 Aug 2014 22:24:01 +0200 Subject: [PATCH 22/57] v0.7.9 Critical bug fix --- app.js | 2 +- backtester.js | 2 +- package.json | 4 ++-- services/profitreporter.js | 4 ++-- services/tradingagent.js | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index b93d554..985e6b9 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.8'); +console.log('Starting BitBot v0.7.9'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index 9866d3e..bc402c9 100644 --- a/backtester.js +++ b/backtester.js @@ -59,7 +59,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.8'); +console.log('Starting BitBot Back-Tester v0.7.9'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/package.json b/package.json index 544e5cf..5e42429 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.7.8", + "version": "0.7.9", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { @@ -27,6 +27,6 @@ "Trading", "Bot" ], - "author": "5an1ty", + "author": "Ruben Callewaert", "license": "MIT" } diff --git a/services/profitreporter.js b/services/profitreporter.js index 73e2ef3..1fb87ee 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -105,7 +105,7 @@ profitreporter.prototype.start = function(resetInitialBalances) { async.series( { balance: function(cb) {api.getBalance(true, cb);}, - orderBook: function(cb) {api.api.getOrderBook(true, cb);} + orderBook: function(cb) {api.getOrderBook(true, cb);} }, this.intialize ); @@ -125,7 +125,7 @@ profitreporter.prototype.updateBalance = function(includeReport) { async.series( { balance: function(cb) {api.getBalance(true, cb);}, - orderBook: function(cb) {api.api.getOrderBook(true, cb);} + orderBook: function(cb) {api.getOrderBook(true, cb);} }, this.processBalance ); diff --git a/services/tradingagent.js b/services/tradingagent.js index 7fa4f50..4a6dfc1 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -44,8 +44,8 @@ tradingagent.prototype.order = function(orderType) { async.series( { - balance: api.getBalance, - orderBook: api.getOrderBook + balance: function(cb) {api.getBalance(true, cb);}, + orderBook: function(cb) {api.getOrderBook(true, cb);} }, process.bind(this) ); From dbcb817f83d81d271422738b0db1f3e9d43423ac Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 24 Aug 2014 23:56:58 +0200 Subject: [PATCH 23/57] v0.8.0 Modular Exchanges, Services Rewrite - Exchanges are now separate modules. - Services no longer read config.js themselves and instead get their settings from app.js. --- README.md | 9 +- app.js | 83 ++-- backtester.js | 51 ++- exchanges/bitstamp.js | 267 ++++++++++++ exchanges/kraken.js | 331 +++++++++++++++ exchanges/template.js | 184 +++++++++ indicators/MACD.js | 4 +- indicators/PPO.js | 4 +- indicators/PSAR.js | 4 +- indicators/{template => template.js} | 9 +- package.json | 10 +- services/api.js | 583 --------------------------- services/candleaggregator.js | 24 +- services/candlestorage.js | 17 +- services/dataprocessor.js | 76 ++-- services/dataretriever.js | 16 +- services/db.js | 91 +++-- services/exchangeapi.js | 135 +++++++ services/loggingservice.js | 14 +- services/ordermonitor.js | 96 +++-- services/pricemonitor.js | 29 +- services/profitreporter.js | 48 ++- services/pushservice.js | 9 +- services/tradingadvisor.js | 24 +- services/tradingagent.js | 56 +-- tests/dbhealth.js | 21 +- {services => util}/tools.js | 0 27 files changed, 1301 insertions(+), 894 deletions(-) create mode 100644 exchanges/bitstamp.js create mode 100644 exchanges/kraken.js create mode 100644 exchanges/template.js rename indicators/{template => template.js} (81%) delete mode 100644 services/api.js create mode 100644 services/exchangeapi.js rename {services => util}/tools.js (100%) diff --git a/README.md b/README.md index 02e00d1..8e3a0b7 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ Please read up on the following articles to help you choose the settings that be As of version 0.7 BitBot now uses indicators as small plugins. You can create your own indicator by using the following template: + //-------------------- REMOVE THIS BLOCK + console.log('If you want this code to do anything, remove this code block!'); + process.exit(); + //-------------------- REMOVE THIS BLOCK + var _ = require('underscore'); var BigNumber = require('bignumber.js'); @@ -101,8 +106,8 @@ As of version 0.7 BitBot now uses indicators as small plugins. You can create yo // Insert your calculation logic here - // When done you should always return either 'buy', 'sell' or 'hold' - return advice; + // When done you should always return either 'buy', 'sell' or 'hold' And either the indicatorResult or the value null + return {advice: advice, indicatorResult: indicatorResult}; }; diff --git a/app.js b/app.js index 985e6b9..c93b0a0 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,9 @@ var _ = require('underscore'); +var loggingservice = require('./services/loggingservice.js'); +var database = require('./services/db.js'); +var candlestorage = require('./services/candlestorage.js'); +var exchangeapiservice = require('./services/exchangeapi.js'); var dataretriever = require('./services/dataretriever.js'); var dataprocessor = require('./services/dataprocessor.js'); var candleaggregator = require('./services/candleaggregator'); @@ -15,25 +19,21 @@ var config = require('./config.js'); //------------------------------Config //------------------------------IntializeModules -var retriever = new dataretriever(config.downloaderRefreshSeconds); -var processor = new dataprocessor(config.candleStickSizeMinutes); -var aggregator = new candleaggregator(config.candleStickSizeMinutes); -var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, false); -var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings); -var pusher = new pushservice(config.pushOver); -var monitor = new ordermonitor(); -var reporter = new profitreporter(config.exchangeSettings.currencyPair); -var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes); +var logger = new loggingservice(config.debug); +var db = new database(config.exchangeSettings, config.mongoConnectionString, logger); +var storage = new candlestorage(db, logger); +var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); +var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); +var processor = new dataprocessor(storage, logger); +var aggregator = new candleaggregator(config.candleStickSizeMinutes, storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, false, storage, logger); +var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings, storage, exchangeapi, logger); +var pusher = new pushservice(config.pushOver, logger); +var monitor = new ordermonitor(exchangeapi, logger); +var reporter = new profitreporter(config.exchangeSettings.currencyPair, db, exchangeapi, logger); +var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes, storage, logger); //------------------------------IntializeModules -//------------------------------AnnounceStart -console.log('------------------------------------------'); -console.log('Starting BitBot v0.7.9'); -console.log('Real Trading Enabled = ' + config.tradingEnabled); -console.log('Working Dir = ' + process.cwd()); -console.log('------------------------------------------'); -//------------------------------AnnounceStart - retriever.on('update', function(ticks){ processor.updateCandleDB(ticks); @@ -74,7 +74,7 @@ aggregator.on('update', function(cs){ } - advisor.update(cs); + advisor.update(cs, false); }); @@ -130,15 +130,19 @@ monitor.on('filled', function(order) { }); -monitor.on('cancelled', function(order) { +monitor.on('cancelled', function(order, retry) { reporter.updateBalance(false); - setTimeout(function(){ + if(retry) { + + cancelledOrderRetryTimeout = setTimeout(function(){ - agent.order(order.orderDetails.orderType); + agent.order(order.orderDetails.orderType); - }, 1000 * 5); + }, 1000 * 5); + + } }); @@ -154,12 +158,6 @@ pricemon.on('advice', function(advice) { } -}); - -reporter.on('update', function(update){ - - - }); reporter.on('report', function(report){ @@ -170,4 +168,31 @@ reporter.on('report', function(report){ }); -processor.initialize(); +var start = function() { + + //------------------------------AnnounceStart + console.log('------------------------------------------'); + console.log('Starting BitBot v0.8.0'); + console.log('Real Trading Enabled = ' + config.tradingEnabled); + console.log('Working Dir = ' + process.cwd()); + console.log('------------------------------------------'); + //------------------------------AnnounceStart + + processor.initialize(); + +}; + +var stop = function(cb) { + + retriever.stop(); + + clearTimeout(cancelledOrderRetryTimeout); + + monitor.resolvePreviousOrder(function(){ + logger.log('BitBot stopped succesfully!'); + cb(); + }); + +}; + +start(); diff --git a/backtester.js b/backtester.js index bc402c9..60dd9d0 100644 --- a/backtester.js +++ b/backtester.js @@ -2,28 +2,33 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); var moment = require('moment'); +var loggingservice = require('./services/loggingservice.js'); +var database = require('./services/db.js'); +var candlestorage = require('./services/candlestorage.js'); +var exchangeapiservice = require('./services/exchangeapi.js'); var dataprocessor = require('./services/dataprocessor.js'); var tradingadvisor = require('./services/tradingadvisor.js'); -var pushservice = require('./services/pushservice.js'); -var storage = require('./services/candlestorage.js'); -var logger = require('./services/loggingservice.js'); var pricemonitor = require('./services/pricemonitor.js'); -var api = require('./services/api.js'); //------------------------------Config var config = require('./config.js'); //------------------------------Config //------------------------------IntializeModules -var processor = new dataprocessor(config.candleStickSizeMinutes); -var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, true); -var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes); +var logger = new loggingservice(config.debug); +var db = new database(config.exchangeSettings, config.mongoConnectionString, logger); +var storage = new candlestorage(db, logger); +var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); +var processor = new dataprocessor(storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, true, storage, logger); +var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes, storage, logger); //------------------------------IntializeModules //------------------------------IntializeVariables var candleStickSizeMinutes = config.candleStickSizeMinutes; var stopLossEnabled = config.stoplossSettings.enabled; var initialBalance = config.backTesting.initialBalance; +var slippagePercentage = config.exchangeSettings.slippagePercentage; var USDBalance = initialBalance; var BTCBalance = 0; var initialBalanceBTC = 0; @@ -42,6 +47,8 @@ var totalFeeCostsPercentage = 0; var transactions = 0; var slTransactions = 0; var lastClose = 0; +var lastClosePlusSlippage = 0; +var lastCloseMinusSlippage = 0; var csPeriod = 0; var entryUSD = 0; var exitUSD = 0; @@ -59,7 +66,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.7.9'); +console.log('Starting BitBot Back-Tester v0.8.0'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart @@ -74,14 +81,16 @@ var createOrder = function(type, stopLoss) { usableBalance = Number(BigNumber(USDBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + lastClosePlusSlippage = Number(BigNumber(lastClose).times(BigNumber(1).plus(BigNumber(slippagePercentage).dividedBy(BigNumber(100)))).round(2)); + totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance)).round(2)); totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(USDBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).round(2))); - BTCBalance = Number(BigNumber(BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(lastClose)).round(2))); + BTCBalance = Number(BigNumber(BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(lastClosePlusSlippage)).round(2))); USDBalance = 0; - var newUSDBalance = Number(BigNumber(BTCBalance).times(BigNumber(lastClose)).round(2)); + var newUSDBalance = Number(BigNumber(BTCBalance).times(BigNumber(lastClosePlusSlippage)).round(2)); if(newUSDBalance > highestUSDValue) { highestUSDValue = newUSDBalance; @@ -96,20 +105,22 @@ var createOrder = function(type, stopLoss) { logger.log('Stop loss Triggered an order:'); } - logger.log('Placed buy order ' + BTCBalance + ' @ ' + lastClose); + logger.log('Placed buy order ' + BTCBalance + ' @ ' + lastClosePlusSlippage); - pricemon.setPosition('bought', lastClose); - advisor.setPosition({pos: 'bought', price: lastClose}); + pricemon.setPosition('bought', lastClosePlusSlippage); + advisor.setPosition({pos: 'bought', price: lastClosePlusSlippage}); } else if(type === 'sell' && BTCBalance !== 0) { usableBalance = Number(BigNumber(BTCBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); - totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance).times(BigNumber(lastClose))).round(2)); + lastCloseMinusSlippage = Number(BigNumber(lastClose).times(BigNumber(1).minus(BigNumber(slippagePercentage).dividedBy(BigNumber(100)))).round(2)); + + totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance).times(BigNumber(lastCloseMinusSlippage))).round(2)); - totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(BTCBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).times(lastClose).round(2))); + totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(BTCBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).times(lastCloseMinusSlippage).round(2))); - USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastClose)).round(2))); + USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastCloseMinusSlippage)).round(2))); BTCBalance = 0; if(USDBalance > highestUSDValue) { @@ -139,10 +150,10 @@ var createOrder = function(type, stopLoss) { logger.log('Stop loss Triggered an order:'); } - logger.log('Placed sell order ' + usableBalance + ' @ ' + lastClose); + logger.log('Placed sell order ' + usableBalance + ' @ ' + lastCloseMinusSlippage); - pricemon.setPosition('sold', lastClose); - advisor.setPosition({pos: 'sold', price: lastClose}); + pricemon.setPosition('sold', lastCloseMinusSlippage); + advisor.setPosition({pos: 'sold', price: lastCloseMinusSlippage}); } else { @@ -154,7 +165,7 @@ var createOrder = function(type, stopLoss) { processor.on('initialized', function(){ - api.getBalance(true, function(err, result){ + exchangeapi.getBalance(true, function(err, result){ transactionFee = result.fee; diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js new file mode 100644 index 0000000..50a8c88 --- /dev/null +++ b/exchanges/bitstamp.js @@ -0,0 +1,267 @@ +var _ = require('underscore'); +var Bitstamp = require('bitstamp-api'); + +var exchange = function(currencyPair, apiSettings, cbManager, logger) { + + this.currencyPair = currencyPair; + + this.bitstamp = new Bitstamp(apiSettings.apiKey, apiSettings.secret, apiSettings.clientId); + + this.cbManager = cbManager; + + this.logger = logger; + + _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + +}; + +exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, callerName, handler) { + + return function(err, result) { + + var cb = this.cbManager(func, receivedArgs, retryAllowed, handler); + + var parsedError = null; + + if(err) { + + if(JSON.stringify(err) === '{}' && err.message) { + parsedError = err.message; + } else { + parsedError = JSON.stringify(err); + } + + this.logger.error(callerName + ': Bitstamp API returned the following error:'); + this.logger.error(parsedError.substring(0,99)); + + if(retryAllowed) { + this.logger.error('Retrying in 15 seconds!'); + } + + } else { + + this.logger.debug(callerName + ': Bitstamp API Call Result (Substring)!'); + this.logger.debug(JSON.stringify(result).substring(0,99)); + + } + + cb(parsedError, result); + + }.bind(this); + +}; + +exchange.prototype.getTrades = function(caller, args, retry, cb) { + + var wrapper = function() { + + var pair = this.currencyPair.pair; + + var handler = function(err, response) { + + if(!err) { + + var trades = _.map(response, function(t) { + + return {date: parseInt(t.date), price: parseFloat(t.price), amount: parseFloat(t.amount)}; + + }); + + var result = _.sortBy(trades, function(trade){ return trade.date; }); + + cb(null, result); + + } else { + + cb(err, null); + + } + + }; + + this.bitstamp.transactions({time: 'hour'}, this.errorHandler(caller, args, retry, 'getTrades', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.getBalance = function(caller, args, retry, cb) { + + var wrapper = function() { + + var asset = this.currencyPair.asset; + var currency = this.currencyPair.currency; + + var pair = this.currencyPair.pair; + + var handler = function(err, result) { + + if(!err) { + + cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); + + } else { + + cb(err, null); + + } + + }; + + this.bitstamp.balance(this.errorHandler(caller, args, retry, 'getBalance', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.getOrderBook = function(caller, args, retry, cb) { + + var wrapper = function () { + + var pair = this.currencyPair.pair; + + var handler = function(err, result) { + + if(!err) { + + var bids = _.map(result.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); + + var asks = _.map(result.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); + + cb(null, {bids: bids, asks: asks}); + + } else { + + cb(err, null); + + } + + }; + + this.bitstamp.order_book(1, this.errorHandler(caller, args, retry, 'getOrderBook', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.placeOrder = function(caller, args, type, amount, price, retry, cb) { + + var wrapper = function() { + + var pair = this.currencyPair.pair; + + var handler = function(err, result) { + + if(!err) { + + cb(null, {txid: result.id}); + + } else { + + cb(err, null); + + } + + }; + + if(type === 'buy') { + + this.bitstamp.buy(amount, price, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + + } else if (type === 'sell') { + + this.bitstamp.sell(amount, price, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + + } else { + + cb(new Error('Invalid order type!'), null); + + } + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.orderFilled = function(caller, args, order, retry, cb) { + + var wrapper = function() { + + var handler = function(err, result) { + + if(!err) { + + var open = _.find(result, function(o) { + + return o.id === order; + + }, this); + + if(open) { + + cb(null, false); + + } else { + + cb(null, true); + + } + + } else { + + cb(err, null); + + } + + }; + + this.bitstamp.open_orders(this.errorHandler(caller, args, retry, 'orderFilled', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { + + var wrapper = function() { + + var handler = function(err, result) { + + if(!err) { + + if(!result.error) { + cb(null, true); + } else { + cb(null, false); + } + + } else { + + cb(err, null); + + } + + }; + + this.bitstamp.cancel_order(order,this.errorHandler(caller, args, retry, 'cancelOrder', handler)); + + }.bind(this); + + return wrapper; + +}; + +module.exports = exchange; diff --git a/exchanges/kraken.js b/exchanges/kraken.js new file mode 100644 index 0000000..7f85fe7 --- /dev/null +++ b/exchanges/kraken.js @@ -0,0 +1,331 @@ +var _ = require('underscore'); +var Kraken = require('kraken-api'); + +var exchange = function(currencyPair, apiSettings, cbManager, logger) { + + this.currencyPair = currencyPair; + + this.kraken = new Kraken(apiSettings.apiKey, apiSettings.secret); + + this.cbManager = cbManager; + + this.logger = logger; + + _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + +}; + + +exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, callerName, handler) { + + return function(err, result) { + + var cb = this.cbManager(func, receivedArgs, retryAllowed, handler); + + var parsedError = null; + + if(err) { + + if(JSON.stringify(err) === '{}' && err.message) { + parsedError = err.message; + } else { + parsedError = JSON.stringify(err); + } + + if(parsedError === '["EQuery:Unknown asset pair"]') { + + this.logger.error(callerName + ': Kraken API returned Unknown asset pair error, exiting!'); + return process.exit(); + + } else { + + this.logger.error(callerName + ': Kraken API returned the following error:'); + this.logger.error(parsedError.substring(0,99)); + + if(retryAllowed) { + this.logger.error('Retrying in 15 seconds!'); + } + + } + + } else { + + this.logger.debug(callerName + ': Kraken API Call Result (Substring)!'); + this.logger.debug(JSON.stringify(result).substring(0,99)); + + } + + cb(parsedError, result); + + }.bind(this); + +}; + +exchange.prototype.getTrades = function(caller, args, retry, cb) { + + var wrapper = function() { + + var pair = this.currencyPair.pair; + + var handler = function(err, data) { + + if(!err) { + + var values = _.find(data.result, function(value, key) { + + return key === pair; + + }); + + var trades = _.map(values, function(t) { + + return {date: parseInt(t[2]), price: parseFloat(t[0]), amount: parseFloat(t[1])}; + + }); + + cb(null, trades); + + } else { + + cb(err, null); + + } + + }; + + this.kraken.api('Trades', {"pair": pair}, this.errorHandler(caller, args, retry, 'getTrades', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.getBalance = function(caller, args, retry, cb) { + + var wrapper = function() { + + var asset = this.currencyPair.asset; + var currency = this.currencyPair.currency; + + var pair = this.currencyPair.pair; + + var handler = function(err, data) { + + if(!err) { + + var assetValue = _.find(data.result, function(value, key) { + return key === asset; + }); + + var currencyValue = _.find(data.result, function(value, key) { + return key === currency; + }); + + if(!assetValue) { + assetValue = 0; + } + + if(!currencyValue) { + currencyValue = 0; + } + + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(caller, args, retry, 'getBalance', function(err, data) { + + if(!err) { + + var fee = parseFloat(_.find(data.result.fees, function(value, key) { + return key === pair; + }).fee); + + cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + + } else { + + cb(err, null); + + } + + })); + + } else { + + cb(err, null); + + } + + }.bind(this); + + this.kraken.api('Balance', {}, this.errorHandler(caller, args, retry, 'getBalance', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.getOrderBook = function(caller, args, retry, cb) { + + var wrapper = function () { + + var pair = this.currencyPair.pair; + + var handler = function(err, data) { + + if(!err) { + + var orderbook = _.find(data.result, function(value, key) { + + return key === pair; + + }); + + var bids = _.map(orderbook.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); + + var asks = _.map(orderbook.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); + + cb(null, {bids: bids, asks: asks}); + + } else { + + cb(err, null); + + } + + }; + + this.kraken.api('Depth', {"pair": pair}, this.errorHandler(caller, args, retry, 'getOrderBook', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.placeOrder = function(caller, args, type, amount, price, retry, cb) { + + var wrapper = function() { + + var pair = this.currencyPair.pair; + + var handler = function(err, data) { + + if(!err) { + + cb(null, {txid: data.result.txid[0]}); + + } else { + + cb(err, null); + + } + + }; + + if(type === 'buy') { + + this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + + } else if (type === 'sell') { + + this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + + } else { + + cb(new Error('Invalid order type!'), null); + + } + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.orderFilled = function(caller, args, order, retry, cb) { + + var wrapper = function() { + + var handler = function(err, data) { + + if(!err) { + + var open = _.find(data.result.open, function(value, key) { + + return key === order; + + }); + + if(open) { + + cb(null, false); + + } else { + + cb(null, true); + + } + + } else { + + cb(err, null); + + } + + }; + + this.kraken.api('OpenOrders', {}, this.errorHandler(caller, args, retry, 'orderFilled', handler)); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { + + var wrapper = function() { + + this.orderFilled(order, true, function(err, filled) { + + if(!filled) { + + var handler = function(err, data) { + + if(!err) { + + if(data.result.count > 0) { + cb(null, true); + } else { + cb(null, false); + } + + } else { + + cb(err, null); + + } + + }; + + this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(caller, args, retry, 'cancelOrder', handler)); + + } else { + + cb(null, false); + + } + + }.bind(this)); + + }.bind(this); + + return wrapper; + +}; + +module.exports = exchange; diff --git a/exchanges/template.js b/exchanges/template.js new file mode 100644 index 0000000..730acfb --- /dev/null +++ b/exchanges/template.js @@ -0,0 +1,184 @@ +//-------------------- REMOVE THIS BLOCK +console.log('If you want this code to do anything, remove this code block!'); +process.exit(); +//-------------------- REMOVE THIS BLOCK + +var _ = require('underscore'); + +var exchange = function(currencyPair, apiSettings, cbManager, logger) { + + this.currencyPair = currencyPair; + + // intialize your API with it's apiSettings here + + this.cbManager = cbManager; + + this.logger = logger; + + _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + +}; + +exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, callerName, handler) { + + return function(err, result) { + + var cb = this.cbManager(func, receivedArgs, retryAllowed, callerName, handler); + + var parsedError = null; + + if(err) { + + if(JSON.stringify(err) === '{}' && err.message) { + parsedError = err.message; + } else { + parsedError = JSON.stringify(err); + } + + this.logger.error(callerName + ' Exchange API returned the following error:'); + this.logger.error(parsedError.substring(0,99)); + + if(retryAllowed) { + this.logger.error('Retrying in 15 seconds!'); + } + + } else { + + this.logger.debug(callerName + ' Exchange API Call Result (Substring)!'); + this.logger.debug(JSON.stringify(result).substring(0,99)); + + } + + cb(parsedError, result); + + }.bind(this); + +}; + +exchange.prototype.getTrades = function(caller, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + cb(null, {date: timestamp, price: number, amount: number}); + + }; + + // Pass this as callback to your exchange function (Expects an Err, Result output). + this.errorHandler(caller, args, retry, 'getTrades', handler); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.getBalance = function(caller, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + cb(null, {currencyAvailable: number, assetAvailable: number, fee: number}); + + }; + + // Pass this as callback to your exchange function (Expects an Err, Result output). + this.errorHandler(caller, args, retry, 'getBalance', handler); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.getOrderBook = function(caller, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + cb(null, {bids: [{assetAmount: number, currencyPrice: number}], asks: [{assetAmount: number, currencyPrice: number}]}); + + }; + + // Pass this as callback to your exchange function (Expects an Err, Result output). + this.errorHandler(caller, args, retry, 'getOrderBook', handler); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.placeOrder = function(caller, type, amount, price, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + cb(null, {txid: transaction_id}); + + }; + + // Pass this as callback to your exchange function (Expects an Err, Result output). + this.errorHandler(caller, args, retry, 'placeOrder', handler); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.orderFilled = function(caller, order, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + cb(null, boolean); + + }; + + // Pass this as callback to your exchange function (Expects an Err, Result output). + this.errorHandler(caller, args, retry, 'orderFilled', handler); + + }.bind(this); + + return wrapper; + +}; + +exchange.prototype.cancelOrder = function(caller, order, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + cb(null, boolean); + + }; + + // Pass this as callback to your exchange function (Expects an Err, Result output). + this.errorHandler(caller, args, retry, 'cancelOrder', handler); + + }.bind(this); + + return wrapper; + +}; + +module.exports = exchange; diff --git a/indicators/MACD.js b/indicators/MACD.js index 46d37a1..9ee8d1f 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -64,11 +64,11 @@ indicator.prototype.calculate = function(cs) { if(this.length >= this.options.neededPeriods) { - return this.advice; + return {advice: this.advice, indicatorValue: this.indicator.result}; } else { - return 'hold'; + return {advice: 'hold', indicatorValue: null}; } diff --git a/indicators/PPO.js b/indicators/PPO.js index 95d93b4..b9b91bf 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -64,11 +64,11 @@ indicator.prototype.calculate = function(cs) { if(this.length >= this.options.neededPeriods) { - return this.advice; + return {advice: this.advice, indicatorValue: this.indicator.result}; } else { - return 'hold'; + return {advice: 'hold', indicatorValue: null}; } diff --git a/indicators/PSAR.js b/indicators/PSAR.js index d16830a..35136e1 100644 --- a/indicators/PSAR.js +++ b/indicators/PSAR.js @@ -121,7 +121,7 @@ indicator.prototype.calculate = function(cs) { this.previousAF = this.options.AFIncrement; this.previousTrend = 1; this.firstCandleDone = true; - return 'hold'; + return {advice: 'hold', indicatorValue: null}; } limit = calculateLimit(this.previousTrend, this.csArray); @@ -162,7 +162,7 @@ indicator.prototype.calculate = function(cs) { this.previousAF = AF; this.previousTrend = trend; - return advice; + return {advice: advice, indicatorValue: PSAR}; }; diff --git a/indicators/template b/indicators/template.js similarity index 81% rename from indicators/template rename to indicators/template.js index ac2aa11..eec76cd 100644 --- a/indicators/template +++ b/indicators/template.js @@ -1,3 +1,8 @@ +//-------------------- REMOVE THIS BLOCK +console.log('If you want this code to do anything, remove this code block!'); +process.exit(); +//-------------------- REMOVE THIS BLOCK + var _ = require('underscore'); var BigNumber = require('bignumber.js'); @@ -26,8 +31,8 @@ indicator.prototype.calculate = function(cs) { // Insert your calculation logic here - // When done you should always return either 'buy', 'sell' or 'hold' - return advice; + // When done you should always return either 'buy', 'sell' or 'hold' And either the indicatorResult or the value null + return {advice: advice, indicatorResult: indicatorResult}; }; diff --git a/package.json b/package.json index 5e42429..b55b5d9 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "name": "BitBot", - "version": "0.7.9", + "version": "0.8.0", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { - "async": "0.2.9", + "async": "0.9.0", "bignumber.js": "1.2.1", - "bitstamp": "0.1.5", + "bitstamp-api": "0.1.1", "mime": "1.2.11", "moment": "2.4.0", - "mongojs": "0.13.1", + "mongojs": "0.14.0", "pushover-notifications": "0.1.5", - "underscore": "1.5.2", + "underscore": "1.6.0", "ws": "0.4.31", "kraken-api": "0.1.5" }, diff --git a/services/api.js b/services/api.js deleted file mode 100644 index ac81b30..0000000 --- a/services/api.js +++ /dev/null @@ -1,583 +0,0 @@ -var _ = require('underscore'); -var logger = require('./loggingservice.js'); -var Bitstamp = require('bitstamp'); -var Kraken = require('kraken-api'); -var async = require('async'); - -//------------------------------Config -var config = require('../config.js'); -//------------------------------Config - -var api = function() { - - this.exchange = config.exchangeSettings.exchange; - this.currencyPair = config.exchangeSettings.currencyPair; - - if(this.exchange === 'bitstamp') { - - var key = config.apiSettings.bitstamp.apiKey; - var secret = config.apiSettings.bitstamp.secret; - var client_id = config.apiSettings.bitstamp.clientId; - - this.bitstamp = new Bitstamp(key, secret, client_id); - - } else if(this.exchange === 'kraken') { - - this.kraken = new Kraken(config.apiSettings.kraken.apiKey, config.apiSettings.kraken.secret); - - } else { - - logger.error('Invalid exchange, exiting!'); - return process.exit(); - - } - - this.q = async.queue(function (task, callback) { - task(); - setTimeout(callback,1000); - }, 1); - - _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); - -}; - -api.prototype.retry = function(method, args) { - - var self = this; - - // make sure the callback (and any other fn) - // is bound to api - _.each(args, function(arg, i) { - if(_.isFunction(arg)) - args[i] = _.bind(arg, self); - }); - - // run the failed method again with the same - // arguments after wait - setTimeout(function() { method.apply(self, args); }, 1000*15); - -}; - -api.prototype.errorHandler = function(method, receivedArgs, retryAllowed, caller, cb) { - - var args = _.toArray(receivedArgs); - - return function(err, result) { - - if(err) { - - var parsedError; - - if(JSON.stringify(err) === '{}' && err.message) { - parsedError = err.message; - } else { - parsedError = JSON.stringify(err); - } - - if(this.exchange === 'kraken' && parsedError === '["EQuery:Unknown asset pair"]') { - - logger.error('Kraken returned Unknown asset pair error, exiting!'); - return process.exit(); - - } else if(retryAllowed) { - - logger.error(caller + ' Couldn\'t connect to the API, retrying in 15 seconds!'); - logger.error(parsedError.substring(0,99)); - return this.retry(method, args); - - } else { - - logger.error(caller + ' Couldn\'t connect to the API.'); - cb(parsedError, null); - return logger.error(parsedError.substring(0,99)); - - } - - } - - if(this.exchange === 'bitstamp' && result.error === 'Invalid nonce') { - logger.error('Bitstamp returned invalid nonce error, retrying in 15 seconds!'); - return this.retry(method, args); - } - - logger.debug('API Call Result (Substring)!'); - logger.debug(JSON.stringify(result).substring(0,99)); - - //_.last(args)(null, result); - cb(null, result); - - }.bind(this); - -}; - -api.prototype.getTrades = function(retry, cb) { - - var args = arguments; - - var wrapper = function() { - - var handler; - - var pair = this.currencyPair.pair; - - if(this.exchange === 'bitstamp') { - - handler = function(err, response) { - - if(!err) { - - var trades = _.map(response, function(t) { - - return {date: parseInt(t.date), price: parseFloat(t.price), amount: parseFloat(t.amount)}; - - }); - - var result = _.sortBy(trades, function(trade){ return trade.date; }); - - cb(null, result); - - } else { - - cb(err, null); - - } - - }; - - this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); - - } else if(this.exchange === 'kraken') { - - handler = function(err, data) { - - if(!err) { - - var values = _.find(data.result, function(value, key) { - - return key === pair; - - }); - - var trades = _.map(values, function(t) { - - return {date: parseInt(t[2]), price: parseFloat(t[0]), amount: parseFloat(t[1])}; - - }); - - cb(null, trades); - - } else { - - cb(err, null); - - } - - }; - - this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); - - } - - }; - - this.q.push(_.bind(wrapper,this)); - -}; - -api.prototype.getBalance = function(retry, cb) { - - var args = arguments; - - var wrapper = function() { - - var handler; - - var asset = this.currencyPair.asset; - var currency = this.currencyPair.currency; - - var pair = this.currencyPair.pair; - - if(this.exchange === 'bitstamp') { - - handler = function(err, result) { - - if(!err) { - - cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); - - } else { - - cb(err, null); - - } - - }; - - this.bitstamp.balance(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); - - } else if(this.exchange === 'kraken') { - - handler = function(err, data) { - - if(!err) { - - var assetValue = _.find(data.result, function(value, key) { - return key === asset; - }); - - var currencyValue = _.find(data.result, function(value, key) { - return key === currency; - }); - - if(!assetValue) { - assetValue = 0; - } - - if(!currencyValue) { - currencyValue = 0; - } - - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, retry, 'getBalance', function(err, data) { - - if(!err) { - - var fee = parseFloat(_.find(data.result.fees, function(value, key) { - return key === pair; - }).fee); - - cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); - - } else { - - cb(err, null); - - } - - })); - - } else { - - cb(err, null); - - } - - }.bind(this); - - this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); - - } - - }; - - this.q.push(_.bind(wrapper,this)); - -}; - -api.prototype.getOrderBook = function(retry, cb) { - - var args = arguments; - - var wrapper = function () { - - var handler; - - var pair = this.currencyPair.pair; - - if(this.exchange === 'bitstamp') { - - handler = function(err, result) { - - if(!err) { - - var bids = _.map(result.bids, function(bid) { - return {assetAmount: bid[1], currencyPrice: bid[0]}; - }); - - var asks = _.map(result.asks, function(ask) { - return {assetAmount: ask[1], currencyPrice: ask[0]}; - }); - - cb(null, {bids: bids, asks: asks}); - - } else { - - cb(err, null); - - } - - }; - - this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); - - } else if(this.exchange === 'kraken') { - - handler = function(err, data) { - - if(!err) { - - var orderbook = _.find(data.result, function(value, key) { - - return key === pair; - - }); - - var bids = _.map(orderbook.bids, function(bid) { - return {assetAmount: bid[1], currencyPrice: bid[0]}; - }); - - var asks = _.map(orderbook.asks, function(ask) { - return {assetAmount: ask[1], currencyPrice: ask[0]}; - }); - - cb(null, {bids: bids, asks: asks}); - - } else { - - cb(err, null); - - } - - }; - - this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); - - } - - }; - - this.q.push(_.bind(wrapper,this)); - -}; - -api.prototype.placeOrder = function(type, amount, price, retry, cb) { - - var args = arguments; - - var wrapper = function() { - - var handler; - - var pair = this.currencyPair.pair; - - if(this.exchange === 'bitstamp') { - - handler = function(err, result) { - - if(!err) { - - cb(null, {txid: result.id}); - - } else { - - cb(err, null); - - } - - }; - - if(type === 'buy') { - - this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); - - } else if (type === 'sell') { - - this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); - - } else { - - logger.log('Invalid order type!'); - } - - } else if(this.exchange === 'kraken') { - - handler = function(err, data) { - - if(!err) { - - cb(null, {txid: data.result.txid[0]}); - - } else { - - cb(err, null); - - } - - }; - - if(type === 'buy') { - - this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); - - } else if (type === 'sell') { - - this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); - - } else { - - logger.log('Invalid order type!'); - - } - - } - - }; - - this.q.push(_.bind(wrapper,this)); - -}; - -api.prototype.orderFilled = function(order, retry, cb) { - - var args = arguments; - - var wrapper = function() { - - var handler; - - if(this.exchange === 'bitstamp') { - - handler = function(err, result) { - - if(!err) { - - var open = _.find(result, function(o) { - - return o.id === order; - - }, this); - - if(open) { - - cb(null, false); - - } else { - - cb(null, true); - - } - - } else { - - cb(err, null); - - } - - }; - - this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); - - } else if(this.exchange === 'kraken') { - - handler = function(err, data) { - - if(!err) { - - var open = _.find(data.result.open, function(value, key) { - - return key === order; - - }); - - if(open) { - - cb(null, false); - - } else { - - cb(null, true); - - } - - } else { - - cb(err, null); - - } - - }; - - this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); - - } - - }; - - this.q.push(_.bind(wrapper,this)); - -}; - -api.prototype.cancelOrder = function(order, retry, cb) { - - var args = arguments; - - var wrapper = function() { - - var handler; - - if(this.exchange === 'bitstamp') { - - handler = function(err, result) { - - if(!err) { - - if(!result.error) { - cb(null, true); - } else { - cb(null, false); - } - - } else { - - cb(err, null); - - } - - }; - - this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); - - } else if(this.exchange === 'kraken') { - - this.orderFilled(order, true, function(err, filled) { - - if(!filled) { - - handler = function(err, data) { - - if(!err) { - - if(data.result.count > 0) { - cb(null, true); - } else { - cb(null, false); - } - - } else { - - cb(err, null); - - } - - }; - - this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); - - } else { - - cb(null, false); - - } - - }.bind(this)); - - } - - }; - - this.q.push(_.bind(wrapper,this)); - -}; - -var apiservice = new api(); - -module.exports = apiservice; diff --git a/services/candleaggregator.js b/services/candleaggregator.js index f25c736..4455ac8 100644 --- a/services/candleaggregator.js +++ b/services/candleaggregator.js @@ -1,11 +1,11 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); -var logger = require('./loggingservice.js'); -var storage = require('./candlestorage.js'); -var aggregator = function(candleStickSize) { +var aggregator = function(candleStickSizeMinutes, storage, logger) { - this.candleStickSize = candleStickSize; + this.storage = storage; + this.candleStickSize = candleStickSizeMinutes; + this.logger = logger; _.bindAll(this, 'update'); @@ -19,28 +19,28 @@ Util.inherits(aggregator, EventEmitter); aggregator.prototype.update = function() { - if(storage.length(this.candleStickSize) > 0) { + if(this.storage.length(this.candleStickSize) > 0) { - this.previousCandlePeriod = storage.getLastNonEmptyPeriod(this.candleStickSize); + this.previousCandlePeriod = this.storage.getLastNonEmptyPeriod(this.candleStickSize); - var cs = storage.getLastCompleteAggregatedCandleStick(this.candleStickSize); + var cs = this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSize); - this.latestCandlePeriod = storage.getLastNonEmptyPeriod(this.candleStickSize); + this.latestCandlePeriod = this.storage.getLastNonEmptyPeriod(this.candleStickSize); if(this.latestCandlePeriod > this.previousCandlePeriod) { - logger.log('Created a new ' + this.candleStickSize + ' minute candlestick!'); - logger.log(JSON.stringify(cs)); + this.logger.log('Created a new ' + this.candleStickSize + ' minute candlestick!'); + this.logger.log(JSON.stringify(cs)); this.emit('update', cs); - storage.removeOldCandles(); + this.storage.removeOldCandles(); } } else { - storage.getFinishedAggregatedCandleSticks(this.candleStickSize); + this.storage.getFinishedAggregatedCandleSticks(this.candleStickSize); } diff --git a/services/candlestorage.js b/services/candlestorage.js index 43ddbe5..b9a50be 100644 --- a/services/candlestorage.js +++ b/services/candlestorage.js @@ -1,11 +1,12 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); -var tools = require('./tools.js'); -var db = require('./db.js'); +var tools = require('../util/tools.js'); -var storage = function() { +var storage = function(db, logger) { this.candleSticksCollection = []; + this.db = db; + this.logger = logger; _.bindAll(this, 'selectCollection', 'set', 'push', 'removeOldCandles', 'flush', 'getAllCandlesSince', 'getLastNCandles', 'getLastPeriod', 'getLastNonEmptyPeriod', 'getLastClose', 'getLastNonEmptyClose', 'getCandle', 'length', 'getAverageCandleStickSize', 'generateWebServerArray', 'getFinishedAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getAggregatedCandleSticks', 'materialise', 'getDBCandles'); @@ -96,7 +97,7 @@ storage.prototype.removeOldCandles = function() { }); if(candleStickSize === 1) { - db.removeOldDBCandles(oldPeriod); + this.db.removeOldDBCandles(oldPeriod); } }, this); @@ -352,11 +353,11 @@ storage.prototype.getAggregatedCandleSticks = function(candleStickSize) { storage.prototype.materialise = function(callback) { var candleStickArray = this.selectCollection(1); - db.materialise(candleStickArray.candleSticks, callback); + this.db.materialise(candleStickArray.candleSticks, callback); }; storage.prototype.getDBCandles = function(callback) { - db.getDBCandles(function(err, storageCandleSticks) { + this.db.getDBCandles(function(err, storageCandleSticks) { if(!err && storageCandleSticks) { this.set(storageCandleSticks); @@ -368,6 +369,4 @@ storage.prototype.getDBCandles = function(callback) { }.bind(this)); }; -var candlestorage = new storage(); - -module.exports = candlestorage; +module.exports = storage; diff --git a/services/dataprocessor.js b/services/dataprocessor.js index d6977c8..eb51a3c 100644 --- a/services/dataprocessor.js +++ b/services/dataprocessor.js @@ -1,15 +1,13 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); var async = require('async'); -var logger = require('./loggingservice.js'); -var storage = require('./candlestorage.js'); -var tools = require('./tools.js'); +var tools = require('../util/tools.js'); -var dataprocessor = function(candleStickSize) { - - this.candleStickSize = candleStickSize; +var processor = function(storage, logger) { this.initialDBWriteDone = false; + this.storage = storage; + this.logger = logger; _.bindAll(this, 'updateCandleStick', 'createBaseCandleSticks', 'processInitialLoad', 'processUpdate', 'initialize', 'updateCandleDB'); @@ -18,10 +16,10 @@ var dataprocessor = function(candleStickSize) { //---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; -Util.inherits(dataprocessor, EventEmitter); +Util.inherits(processor, EventEmitter); //---EventEmitter Setup -dataprocessor.prototype.updateCandleStick = function (candleStick, tick) { +processor.prototype.updateCandleStick = function (candleStick, tick) { if(!candleStick.open) { @@ -51,7 +49,7 @@ dataprocessor.prototype.updateCandleStick = function (candleStick, tick) { }; -dataprocessor.prototype.createBaseCandleSticks = function (callback) { +processor.prototype.createBaseCandleSticks = function (callback) { var previousClose = 0; @@ -61,7 +59,7 @@ dataprocessor.prototype.createBaseCandleSticks = function (callback) { var tickTimeStamp = this.ticks[0].date; - var lastStoragePeriod = storage.getLastNonEmptyPeriod(); + var lastStoragePeriod = this.storage.getLastNonEmptyPeriod(); var firstTickCandleStick = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); if(lastStoragePeriod < firstTickCandleStick && lastStoragePeriod !== 0) { @@ -77,9 +75,9 @@ dataprocessor.prototype.createBaseCandleSticks = function (callback) { while(endTimeStamp < this.ticks[0].date) { - previousClose = storage.getLastNonEmptyClose(); + previousClose = this.storage.getLastNonEmptyClose(); - storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + this.storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); startTimeStamp = endTimeStamp; endTimeStamp = endTimeStamp + candleStickSizeSeconds; @@ -95,22 +93,22 @@ dataprocessor.prototype.createBaseCandleSticks = function (callback) { while(tickTimeStamp >= endTimeStamp + candleStickSizeSeconds) { if(currentCandleStick.volume > 0) { - storage.push(currentCandleStick); + this.storage.push(currentCandleStick); } startTimeStamp = endTimeStamp; endTimeStamp = endTimeStamp + candleStickSizeSeconds; - previousClose = storage.getLastNonEmptyClose(); + previousClose = this.storage.getLastNonEmptyClose(); - storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + this.storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); } if(tickTimeStamp >= endTimeStamp) { if(currentCandleStick.volume > 0) { - storage.push(currentCandleStick); + this.storage.push(currentCandleStick); } startTimeStamp = endTimeStamp; @@ -130,7 +128,7 @@ dataprocessor.prototype.createBaseCandleSticks = function (callback) { if(currentCandleStick.volume > 0) { - storage.push(currentCandleStick); + this.storage.push(currentCandleStick); startTimeStamp = endTimeStamp; endTimeStamp = endTimeStamp + candleStickSizeSeconds; @@ -142,9 +140,9 @@ dataprocessor.prototype.createBaseCandleSticks = function (callback) { var beginPeriod = i; var endPeriod = beginPeriod + candleStickSizeSeconds; - previousClose = storage.getLastNonEmptyClose(); + previousClose = this.storage.getLastNonEmptyClose(); - storage.push({'period':beginPeriod,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + this.storage.push({'period':beginPeriod,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); } @@ -158,12 +156,18 @@ dataprocessor.prototype.createBaseCandleSticks = function (callback) { }; -dataprocessor.prototype.processInitialLoad = function(err, result) { +processor.prototype.processInitialLoad = function(err, result) { if(err) { - logger.error('Couldn\'t create candlesticks due to a database error'); - logger.error(err.stack); + var parsedError = err; + + if(err.stack) { + parsedError = err.stack; + } + + this.logger.error('Couldn\'t create candlesticks due to a database error'); + this.logger.error(parsedError); process.exit(); @@ -175,20 +179,26 @@ dataprocessor.prototype.processInitialLoad = function(err, result) { }; -dataprocessor.prototype.processUpdate = function(err, result) { +processor.prototype.processUpdate = function(err, result) { this.ticks = []; if(err) { - logger.error('Couldn\'t create candlesticks due to a database error'); - logger.error(err.stack); + var parsedError = err; + + if(err.stack) { + parsedError = err.stack; + } + + this.logger.error('Couldn\'t create candlesticks due to a database error'); + this.logger.error(parsedError); process.exit(); } else { - var latestCandleStick = storage.getLastNCandles(1)[0]; + var latestCandleStick = this.storage.getLastNCandles(1)[0]; if(!this.initialDBWriteDone) { this.emit('initialDBWrite'); @@ -203,17 +213,17 @@ dataprocessor.prototype.processUpdate = function(err, result) { }; -dataprocessor.prototype.initialize = function() { +processor.prototype.initialize = function() { async.waterfall([ - storage.getDBCandles + this.storage.getDBCandles ], this.processInitialLoad); - }; +}; -dataprocessor.prototype.updateCandleDB = function(ticks) { +processor.prototype.updateCandleDB = function(ticks) { - var period = storage.getLastNonEmptyPeriod(); + var period = this.storage.getLastNonEmptyPeriod(); this.ticks = _.filter(ticks,function(tick){ @@ -223,9 +233,9 @@ dataprocessor.prototype.updateCandleDB = function(ticks) { async.waterfall([ this.createBaseCandleSticks, - storage.materialise + this.storage.materialise ], this.processUpdate); }; -module.exports = dataprocessor; +module.exports = processor; diff --git a/services/dataretriever.js b/services/dataretriever.js index 5c9714e..fd201ed 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -1,12 +1,12 @@ var _ = require('underscore'); var async = require('async'); -var logger = require('./loggingservice.js'); -var api = require('./api.js'); -var downloader = function(refreshInterval){ +var downloader = function(refreshInterval, exchangeapi, logger){ this.refreshInterval = refreshInterval; this.noTradesCount = 0; + this.exchangeapi = exchangeapi; + this.logger = logger; _.bindAll(this, 'start', 'stop', 'processTrades'); @@ -31,7 +31,7 @@ downloader.prototype.processTrades = function(err, trades) { this.noTradesCount += 1; if(this.noTradesCount >= 30) { - logger.error('Haven\'t received data from the API for 30 consecutive attempts, stopping application'); + this.logger.error('Haven\'t received data from the Exchange API for 30 consecutive attempts, stopping application'); return process.exit(); } @@ -41,12 +41,12 @@ downloader.prototype.processTrades = function(err, trades) { downloader.prototype.start = function() { - logger.log('Downloader started!'); + this.logger.log('Downloader started!'); - api.getTrades(false, this.processTrades); + this.exchangeapi.getTrades(false, this.processTrades); this.downloadInterval = setInterval(function(){ - api.getTrades(false, this.processTrades); + this.exchangeapi.getTrades(false, this.processTrades); }.bind(this),1000 * this.refreshInterval); }; @@ -55,7 +55,7 @@ downloader.prototype.stop = function() { clearInterval(this.downloadInterval); - logger.log('Downloader stopped!'); + this.logger.log('Downloader stopped!'); }; diff --git a/services/db.js b/services/db.js index 9725ed5..862984d 100644 --- a/services/db.js +++ b/services/db.js @@ -2,14 +2,13 @@ var _ = require('underscore'); var mongo = require('mongojs'); var async = require('async'); -//------------------------------Config -var config = require('../config.js'); -//------------------------------Config +var db = function(exchangeSettings, mongoConnectionString, logger) { -var db = function() { - - this.pair = config.exchangeSettings.currencyPair.pair; - this.dbCollectionName = config.exchangeSettings.exchange + config.exchangeSettings.currencyPair.pair; + this.pair = exchangeSettings.currencyPair.pair; + this.exchange = exchangeSettings.exchange; + this.dbCollectionName = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; + this.mongoConnectionString = mongoConnectionString; + this.logger = logger; _.bindAll(this, 'materialise', 'removeOldDBCandles', 'getDBCandles', 'getInitialBalance', 'setInitialBalance'); @@ -17,62 +16,70 @@ var db = function() { db.prototype.materialise = function(candleStickArray, callback) { - var csDatastore = mongo(config.mongoConnectionString); + var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection(this.dbCollectionName); csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { - var filterPeriod = 0; + if(err) { - if(!err && sticks.length > 0) { + callback(err); - filterPeriod = sticks[0].period; + } else { - } + var filterPeriod = 0; - materialiseCs = _.filter(candleStickArray, function(cs){ + if(sticks.length > 0) { - return cs.period >= filterPeriod; + filterPeriod = sticks[0].period; - }); + } - if(materialiseCs.length > 0) { + materialiseCs = _.filter(candleStickArray, function(cs){ - async.eachSeries(materialiseCs, function(cs, cb) { + return cs.period >= filterPeriod; - csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, doc) { + }); - if(err) { + if(materialiseCs.length > 0) { - cb(err); + async.eachSeries(materialiseCs, function(cs, cb) { - } else { + csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, doc) { - cb(); + if(err) { - } + cb(err); - }); + } else { + + cb(); - }, function(err) { + } - csDatastore.close(); + }); - if(err) { + }, function(err) { - callback(err); + csDatastore.close(); - } else { + if(err) { - callback(null); + callback(err); - } + } else { - }); + callback(null); - } else { + } - callback(null); + }); + + } else { + + callback(null); + + } } @@ -82,7 +89,7 @@ db.prototype.materialise = function(candleStickArray, callback) { db.prototype.removeOldDBCandles = function(filterPeriod) { - var csDatastore = mongo(config.mongoConnectionString); + var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection(this.dbCollectionName); csCollection.remove({ period: { $lte: filterPeriod } }, function(err, resp) { @@ -95,7 +102,7 @@ db.prototype.removeOldDBCandles = function(filterPeriod) { db.prototype.getDBCandles = function(callback) { - var csDatastore = mongo(config.mongoConnectionString); + var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection(this.dbCollectionName); csCollection.ensureIndex({period: 1}); @@ -128,10 +135,10 @@ db.prototype.getDBCandles = function(callback) { db.prototype.getInitialBalance = function(callback) { - var csDatastore = mongo(config.mongoConnectionString); + var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection('balance'); - csCollection.find({pair: this.pair}).limit(1, function(err, balance) { + csCollection.find({exchangePair: this.dbCollectionName}).limit(1, function(err, balance) { csDatastore.close(); @@ -157,10 +164,10 @@ db.prototype.getInitialBalance = function(callback) { db.prototype.setInitialBalance = function(initialBalance, callback) { - var csDatastore = mongo(config.mongoConnectionString); + var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection('balance'); - csCollection.update({pair: this.pair}, {pair: this.pair, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { + csCollection.update({exchangePair: this.dbCollectionName}, {exchangePair: this.dbCollectionName, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { csDatastore.close(); @@ -178,6 +185,4 @@ db.prototype.setInitialBalance = function(initialBalance, callback) { }; -var database = new db(); - -module.exports = database; +module.exports = db; diff --git a/services/exchangeapi.js b/services/exchangeapi.js new file mode 100644 index 0000000..1ef0e80 --- /dev/null +++ b/services/exchangeapi.js @@ -0,0 +1,135 @@ +var _ = require('underscore'); +var async = require('async'); +var fs = require('fs'); + +var api = function(exchangeSettings, apiSettings, logger) { + + this.exchange = exchangeSettings.exchange; + this.currencyPair = exchangeSettings.currencyPair; + this.logger = logger; + + if(fs.existsSync('./exchanges/' + this.exchange + '.js')) { + var Exchange = require('../exchanges/' + this.exchange + '.js'); + this.selectedExchange = new Exchange(this.currencyPair, apiSettings[this.exchange], this.cbManager.bind(this), logger); + } else { + var err = new Error('Wrong exchange chosen. This exchange doesn\'t exist.'); + this.logger.error(err.stack); + process.exit(); + } + + this.q = async.queue(function (task, callback) { + task(); + setTimeout(callback,1000); + }, 1); + + _.bindAll(this, 'retry', 'cbManager', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + +}; + +api.prototype.retry = function(method, args) { + + var self = this; + + // make sure the callback (and any other fn) + // is bound to api + _.each(args, function(arg, i) { + if(_.isFunction(arg)) + args[i] = _.bind(arg, self); + }); + + // run the failed method again with the same + // arguments after wait + + setTimeout(function() { method.apply(self, args); }, 1000*15); + +}; + +api.prototype.cbManager = function(method, receivedArgs, retryAllowed, cb) { + + var args = _.toArray(receivedArgs); + + return function(err, result) { + + if(err) { + + if(retryAllowed) { + + return this.retry(method, args); + + } else { + + return cb(err, null); + + } + + } else { + + cb(null, result); + + } + + }.bind(this); + +}; + +api.prototype.getTrades = function(retry, cb) { + + var args = arguments; + + var wrapper = this.selectedExchange.getTrades(this.getTrades, args, retry, cb); + + this.q.push(wrapper); + +}; + +api.prototype.getBalance = function(retry, cb) { + + var args = arguments; + + var wrapper = this.selectedExchange.getBalance(this.getBalance, args, retry, cb); + + this.q.push(wrapper); + +}; + +api.prototype.getOrderBook = function(retry, cb) { + + var args = arguments; + + var wrapper = this.selectedExchange.getOrderBook(this.getOrderBook, args, retry, cb); + + this.q.push(wrapper); + +}; + +api.prototype.placeOrder = function(type, amount, price, retry, cb) { + + var args = arguments; + + var wrapper = this.selectedExchange.placeOrder(this.placeOrder, args, type, amount, price, retry, cb); + + this.q.push(wrapper); + +}; + +api.prototype.orderFilled = function(order, retry, cb) { + + var args = arguments; + + var wrapper = this.selectedExchange.orderFilled(this.orderFilled, args, order, retry, cb); + + this.q.push(wrapper); + +}; + +api.prototype.cancelOrder = function(order, retry, cb) { + + var args = arguments; + + var wrapper = this.selectedExchange.cancelOrder(this.cancelOrder, args, order, retry, cb); + + this.q.push(wrapper); + +}; + +module.exports = api; diff --git a/services/loggingservice.js b/services/loggingservice.js index 75868f1..40dd169 100644 --- a/services/loggingservice.js +++ b/services/loggingservice.js @@ -1,13 +1,9 @@ var moment = require('moment'); var _ = require('underscore'); -//------------------------------Config -var config = require('../config.js'); -//------------------------------Config +var logger = function(debug) { -var logger = function() { - - this.debugEnabled = config.debug; + this.debugEnabled = debug; _.bindAll(this, 'log', 'debug', 'error'); @@ -16,7 +12,7 @@ var logger = function() { logger.prototype.log = function(message) { var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - + console.log('[' + now + '] (INFO) ' + message); }; @@ -41,6 +37,4 @@ logger.prototype.error = function(message) { }; -var loggingservice = new logger(); - -module.exports = loggingservice; +module.exports = logger; diff --git a/services/ordermonitor.js b/services/ordermonitor.js index 28e4e1a..16606c3 100644 --- a/services/ordermonitor.js +++ b/services/ordermonitor.js @@ -1,23 +1,24 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); -var logger = require('./loggingservice.js'); -var api = require('./api.js'); -var ordermonitor = function() { +var monitor = function(exchangeapi, logger) { + + this.exchangeapi = exchangeapi; + this.logger = logger; _.bindAll(this, 'checkFilled', 'processCancellation', 'processSimulation', 'add', 'resolvePreviousOrder'); - this.checkOrder = {}; + this.checkOrder = {status: 'resolved'}; }; //---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; -Util.inherits(ordermonitor, EventEmitter); +Util.inherits(monitor, EventEmitter); //---EventEmitter Setup -ordermonitor.prototype.checkFilled = function(checkOrder, filled) { +monitor.prototype.checkFilled = function(checkOrder, filled) { if(checkOrder.status !== 'filled') { @@ -28,7 +29,7 @@ ordermonitor.prototype.checkFilled = function(checkOrder, filled) { clearInterval(checkOrder.interval); clearTimeout(checkOrder.timeout); - logger.log('Order (' + checkOrder.id + ') filled succesfully!'); + this.logger.log('Order (' + checkOrder.id + ') filled succesfully!'); this.emit('filled', checkOrder); @@ -38,21 +39,21 @@ ordermonitor.prototype.checkFilled = function(checkOrder, filled) { }; -ordermonitor.prototype.processCancellation = function(checkOrder, cancelled) { +monitor.prototype.processCancellation = function(checkOrder, cancelled, retry) { if(cancelled && checkOrder.status !== 'cancelled') { checkOrder.status = 'cancelled'; - logger.log('Order (' + checkOrder.id + ') cancelled!'); + this.logger.log('Order (' + checkOrder.id + ') cancelled!'); - this.emit('cancelled', checkOrder); + this.emit('cancelled', checkOrder, retry); } else if(checkOrder.status !== 'filled') { checkOrder.status = 'filled'; - logger.log('Order (' + checkOrder.id + ') filled succesfully!'); + this.logger.log('Order (' + checkOrder.id + ') filled succesfully!'); this.emit('filled', checkOrder); @@ -60,9 +61,9 @@ ordermonitor.prototype.processCancellation = function(checkOrder, cancelled) { }; -ordermonitor.prototype.processSimulation = function(checkOrder) { +monitor.prototype.processSimulation = function(checkOrder) { - logger.log('Order (' + checkOrder.id + ') filled succesfully!'); + this.logger.log('Order (' + checkOrder.id + ') filled succesfully!'); checkOrder.status = 'filled'; @@ -70,61 +71,74 @@ ordermonitor.prototype.processSimulation = function(checkOrder) { }; -ordermonitor.prototype.add = function(orderDetails, cancelTime) { +monitor.prototype.add = function(orderDetails, cancelTime) { - this.resolvePreviousOrder(); + var wrapper = function() { - this.checkOrder = {id:orderDetails.order, orderDetails:orderDetails, status:'open'}; + this.checkOrder = {id:orderDetails.order, orderDetails:orderDetails, status:'open'}; - logger.log('Monitoring order: ' + this.checkOrder.id + ' (Cancellation after ' + cancelTime + ' minutes)'); + this.logger.log('Monitoring order: ' + this.checkOrder.id + ' (Cancellation after ' + cancelTime + ' minutes)'); - if(this.checkOrder.id === 'Simulated') { + if(this.checkOrder.id === 'Simulated') { - this.processSimulation(this.checkOrder); + this.processSimulation(this.checkOrder); - } else { + } else { - this.checkOrder.interval = setInterval(function() { + this.checkOrder.interval = setInterval(function() { - api.orderFilled(this.checkOrder.id, false, function(err, response){ - if(!err) { - this.checkFilled(this.checkOrder, response); - } - }.bind(this)); + this.exchangeapi.orderFilled(this.checkOrder.id, false, function(err, response){ + if(!err) { + this.checkFilled(this.checkOrder, response); + } + }.bind(this)); - }.bind(this), 1000 * 10); + }.bind(this), 1000 * 10); - this.checkOrder.timeout = setTimeout(function() { + this.checkOrder.timeout = setTimeout(function() { - clearInterval(this.checkOrder.interval); + clearInterval(this.checkOrder.interval); - if(this.checkOrder.status === 'open') { + if(this.checkOrder.status === 'open') { - logger.log('Cancelling order: ' + this.checkOrder.id); + this.logger.log('Cancelling order: ' + this.checkOrder.id); - api.cancelOrder(this.checkOrder.id, true, function(err, response) { - this.processCancellation(this.checkOrder, response); - }.bind(this)); + this.exchangeapi.cancelOrder(this.checkOrder.id, true, function(err, response) { + this.processCancellation(this.checkOrder, response, true); + }.bind(this)); + + } - } + }.bind(this), 1000 * 60 * cancelTime); - }.bind(this), 1000 * 60 * cancelTime); + } - } + }.bind(this); + + this.resolvePreviousOrder(wrapper); }; -ordermonitor.prototype.resolvePreviousOrder = function() { +monitor.prototype.resolvePreviousOrder = function(cb) { - if(this.checkOrder.status === 'open') { + if(this.checkOrder.status === 'open' && this.checkOrder.id !== 'Simulated') { clearInterval(this.checkOrder.interval); clearTimeout(this.checkOrder.timeout); - this.checkOrder.status = 'resolved'; + this.logger.log('Cancelling order: ' + this.checkOrder.id); + + this.exchangeapi.cancelOrder(this.checkOrder.id, true, function(err, response) { + this.processCancellation(this.checkOrder, response, false); + this.checkOrder.status = 'resolved'; + this.logger.log('Previous order (' + this.checkOrder.id + ') resolved!'); + cb(); + }.bind(this)); + } else { + cb(); } }; -module.exports = ordermonitor; +module.exports = monitor; diff --git a/services/pricemonitor.js b/services/pricemonitor.js index 745dccd..7a593c8 100644 --- a/services/pricemonitor.js +++ b/services/pricemonitor.js @@ -1,14 +1,13 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); -var logger = require('./loggingservice.js'); -var api = require('./api.js'); -var storage = require('./candlestorage.js'); -var pricemonitor = function(slPercentageB, slPercentageS, candleStickSizeMinutes) { +var monitor = function(slPercentageB, slPercentageS, candleStickSizeMinutes, storage, logger) { this.percentageBought = slPercentageB; this.percentageSold = slPercentageS; this.candleStickSizeMinutes = candleStickSizeMinutes; + this.storage = storage; + this.logger = logger; this.position = 'none'; @@ -19,24 +18,24 @@ var pricemonitor = function(slPercentageB, slPercentageS, candleStickSizeMinutes //---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; -Util.inherits(pricemonitor, EventEmitter); +Util.inherits(monitor, EventEmitter); //---EventEmitter Setup -pricemonitor.prototype.check = function(price) { +monitor.prototype.check = function(price) { if(this.position === 'bought') { if(price <= this.checkPriceBought) { - logger.log('Stop Loss triggered (Long Entry: ' + this.posPrice + ' Exit: ' + price + ')'); + this.logger.log('Stop Loss triggered (Long Entry: ' + this.posPrice + ' Exit: ' + price + ')'); this.position = 'none'; this.posPrice = 0; this.emit('advice', 'sell'); } - + } else if(this.position === 'sold') { if(price >= this.checkPriceSold) { - logger.log('Stop Loss triggered (Short Entry: ' + this.posPrice + ' Exit: ' + price + ')'); + this.logger.log('Stop Loss triggered (Short Entry: ' + this.posPrice + ' Exit: ' + price + ')'); this.position = 'none'; this.posPrice = 0; this.emit('advice', 'buy'); @@ -50,7 +49,7 @@ pricemonitor.prototype.check = function(price) { }; -pricemonitor.prototype.setPosition = function(pos, price) { +monitor.prototype.setPosition = function(pos, price) { if(pos === 'bought') { @@ -68,11 +67,11 @@ pricemonitor.prototype.setPosition = function(pos, price) { }; -pricemonitor.prototype.update = function(cs) { +monitor.prototype.update = function(cs) { var diff = cs.close - cs.open; var size = Math.abs(Number(BigNumber(cs.close).minus(BigNumber(cs.open)).round(2))); - var averageSize = storage.getAverageCandleStickSize(10, this.candleStickSizeMinutes); + var averageSize = this.storage.getAverageCandleStickSize(10, this.candleStickSizeMinutes); var change = Number(BigNumber(size).dividedBy(2).round(2)); @@ -84,7 +83,7 @@ pricemonitor.prototype.update = function(cs) { newSl = Number(BigNumber(this.checkPriceBought).plus(change)); - logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); + this.logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); this.checkPriceBought = newSl; @@ -92,7 +91,7 @@ pricemonitor.prototype.update = function(cs) { newSl = Number(BigNumber(this.checkPriceSold).minus(change)); - logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); + this.logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); this.checkPriceSold = newSl; @@ -102,4 +101,4 @@ pricemonitor.prototype.update = function(cs) { }; -module.exports = pricemonitor; +module.exports = monitor; diff --git a/services/profitreporter.js b/services/profitreporter.js index 1fb87ee..8959f59 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -1,13 +1,13 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); var async = require('async'); -var logger = require('./loggingservice.js'); -var db = require('./db.js'); -var api = require('./api.js'); -var profitreporter = function(currencyPair) { +var reporter = function(currencyPair, db, exchangeapi, logger) { this.currencyPair = currencyPair; + this.db = db; + this.exchangeapi = exchangeapi; + this.logger = logger; _.bindAll(this, 'intialize', 'createReport', 'processBalance', 'start', 'updateBalance'); @@ -16,10 +16,10 @@ var profitreporter = function(currencyPair) { //---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; -Util.inherits(profitreporter, EventEmitter); +Util.inherits(reporter, EventEmitter); //---EventEmitter Setup -profitreporter.prototype.intialize = function(err, result) { +reporter.prototype.intialize = function(err, result) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); @@ -29,19 +29,19 @@ profitreporter.prototype.intialize = function(err, result) { this.initalTotalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); - db.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { + this.db.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { if(err) { - logger.error('Couldn\'t get initialBalance due to a database error'); - logger.error(err.stack); + this.logger.error('Couldn\'t get initialBalance due to a database error'); + this.logger.error(err.stack); process.exit(); } else { if(this.resetInitialBalances) { - logger.log(this.currencyPair.pair + ' Balance reset successfully, change the configuration setting back to false and restart the application.'); + this.logger.log(this.currencyPair.pair + ' Balance reset successfully, change the configuration setting back to false and restart the application.'); process.exit(); } @@ -51,17 +51,17 @@ profitreporter.prototype.intialize = function(err, result) { }; -profitreporter.prototype.createReport = function() { +reporter.prototype.createReport = function() { var report = this.currencyPair.asset + ': ' + this.assetBalance + ' ' + this.currencyPair.currency + ': ' + this.currencyBalance + ' Total in ' + this.currencyPair.currency + ': ' + this.totalCurrencyBalance + ' Profit: ' + this.profitAbsolute + ' (' + this.profitPercentage + '%)'; - logger.log('Profit Report: ' + report); + this.logger.log('Profit Report: ' + report); this.emit('report', report); }; -profitreporter.prototype.processBalance = function(err, result) { +reporter.prototype.processBalance = function(err, result) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); @@ -77,20 +77,18 @@ profitreporter.prototype.processBalance = function(err, result) { this.createReport(); } - this.emit('update', {'asset': this.currencyPair.asset, 'currency': this.currencyPair.currency, 'currencyBalance': this.currencyBalance, 'assetBalance': this.assetBalance, 'profitAbsolute': this.profitAbsolute, 'profitPercentage': this.profitPercentage}); - }; -profitreporter.prototype.start = function(resetInitialBalances) { +reporter.prototype.start = function(resetInitialBalances) { this.resetInitialBalances = resetInitialBalances; - db.getInitialBalance(function(err, result) { + this.db.getInitialBalance(function(err, result) { if(err) { - logger.error('Couldn\'t get initialBalance due to a database error'); - logger.error(err.stack); + this.logger.error('Couldn\'t get initialBalance due to a database error'); + this.logger.error(err.stack); process.exit(); @@ -104,8 +102,8 @@ profitreporter.prototype.start = function(resetInitialBalances) { async.series( { - balance: function(cb) {api.getBalance(true, cb);}, - orderBook: function(cb) {api.getOrderBook(true, cb);} + balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this) }, this.intialize ); @@ -118,18 +116,18 @@ profitreporter.prototype.start = function(resetInitialBalances) { }; -profitreporter.prototype.updateBalance = function(includeReport) { +reporter.prototype.updateBalance = function(includeReport) { this.includeReport = includeReport; async.series( { - balance: function(cb) {api.getBalance(true, cb);}, - orderBook: function(cb) {api.getOrderBook(true, cb);} + balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this) }, this.processBalance ); }; -module.exports = profitreporter; +module.exports = reporter; diff --git a/services/pushservice.js b/services/pushservice.js index 625ae21..36f1b57 100644 --- a/services/pushservice.js +++ b/services/pushservice.js @@ -1,8 +1,9 @@ var _ = require('underscore'); var push = require( 'pushover-notifications' ); -var logger = require('./loggingservice.js'); -var pusher = function(pushOver) { +var pusher = function(pushOver, logger) { + + this.logger = logger; if(pushOver.pushUserId && pushOver.pushAppToken) { @@ -42,12 +43,12 @@ pusher.prototype.send = function(title, message, sound, priority) { throw err; } - logger.log('Push notification sent!'); + this.logger.log('Push notification sent!'); }); } else { - logger.log('Push Service Misconfigured'); + this.logger.log('Push Service Misconfigured'); } diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index 370791a..396706f 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -1,20 +1,20 @@ var _ = require('underscore'); var async = require('async'); -var logger = require('./loggingservice.js'); -var storage = require('./candlestorage.js'); var fs = require('fs'); -var advisor = function(indicatorSettings, candleStickSize, backTesting) { +var advisor = function(indicatorSettings, candleStickSize, backTesting, storage, logger) { this.candleStickSize = candleStickSize; this.backTesting = backTesting; + this.storage = storage; + this.logger = logger; if(fs.existsSync('./indicators/' + indicatorSettings.indicator + '.js')) { var indicator = require('../indicators/' + indicatorSettings.indicator + '.js'); this.selectedIndicator = new indicator(indicatorSettings.options); } else { var err = new Error('Wrong indicator chosen. This indicator doesn\'t exist.'); - logger.error(err.stack); + this.logger.error(err.stack); process.exit(); } @@ -30,11 +30,11 @@ Util.inherits(advisor, EventEmitter); advisor.prototype.start = function() { - var candleSticks = storage.getFinishedAggregatedCandleSticks(this.candleStickSize); + var candleSticks = this.storage.getFinishedAggregatedCandleSticks(this.candleStickSize); for(var i = 0; i < candleSticks.length; i++) { - var advice = this.selectedIndicator.calculate(candleSticks[i]); + var result = this.selectedIndicator.calculate(candleSticks[i]); } @@ -42,19 +42,19 @@ advisor.prototype.start = function() { advisor.prototype.update = function(cs) { - var advice = this.selectedIndicator.calculate(cs); + var result = this.selectedIndicator.calculate(cs); if(!this.backTesting) { - logger.log('Advice: ' + advice); + this.logger.log('Advice: ' + result.advice + ' (' + result.indicatorValue + ')'); } else { - logger.debug('Advice: ' + advice); + this.logger.debug('Advice: ' + result.advice + ' (' + result.indicatorValue + ')'); } - if(['buy', 'sell', 'hold'].indexOf(advice) >= 0) { - this.emit('advice', advice); + if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { + this.emit('advice', result.advice); } else { var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); - logger.error(err.stack); + this.logger.error(err.stack); process.exit(); } diff --git a/services/tradingagent.js b/services/tradingagent.js index 4a6dfc1..59800d7 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -1,29 +1,29 @@ var _ = require('underscore'); var BigNumber = require('bignumber.js'); var async = require('async'); -var logger = require('./loggingservice.js'); -var storage = require('./candlestorage.js'); -var api = require('./api.js'); -var tradingagent = function(tradingEnabled, exchangeSettings) { +var agent = function(tradingEnabled, exchangeSettings, storage, exchangeapi, logger) { _.bindAll(this, 'order', 'calculateOrder', 'placeRealOrder', 'placeSimulatedOrder', 'processOrder'); - this.tradingEnabled = tradingEnabled; - this.currencyPair = exchangeSettings.currencyPair; - this.tradingReserveAsset = exchangeSettings.tradingReserveAsset; - this.tradingReserveCurrency = exchangeSettings.tradingReserveCurrency; - this.slippagePercentage = exchangeSettings.slippagePercentage; + this.tradingEnabled = tradingEnabled; + this.currencyPair = exchangeSettings.currencyPair; + this.tradingReserveAsset = exchangeSettings.tradingReserveAsset; + this.tradingReserveCurrency = exchangeSettings.tradingReserveCurrency; + this.slippagePercentage = exchangeSettings.slippagePercentage; + this.storage = storage; + this.exchangeapi = exchangeapi; + this.logger = logger; }; //---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; -Util.inherits(tradingagent, EventEmitter); +Util.inherits(agent, EventEmitter); //---EventEmitter Setup -tradingagent.prototype.order = function(orderType) { +agent.prototype.order = function(orderType) { this.orderDetails = {}; @@ -44,15 +44,15 @@ tradingagent.prototype.order = function(orderType) { async.series( { - balance: function(cb) {api.getBalance(true, cb);}, - orderBook: function(cb) {api.getOrderBook(true, cb);} + balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this) }, process.bind(this) ); }; -tradingagent.prototype.calculateOrder = function(result) { +agent.prototype.calculateOrder = function(result) { this.orderDetails.assetBalance = parseFloat(result.balance.assetAvailable); this.orderDetails.currencyBalance = parseFloat(result.balance.currencyAvailable); @@ -60,11 +60,11 @@ tradingagent.prototype.calculateOrder = function(result) { var orderBook = result.orderBook; - var lastClose = storage.getLastClose(); + var lastClose = this.storage.getLastClose(); var minClose = Number(BigNumber(lastClose).times(BigNumber(0.9975)).round(2)); var maxClose = Number(BigNumber(lastClose).times(BigNumber(1.0025)).round(2)); - logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.tradingFee +')'); + this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.tradingFee +')'); if(this.orderDetails.orderType === 'buy') { @@ -82,7 +82,7 @@ tradingagent.prototype.calculateOrder = function(result) { var lowestAskWithSlippage = Number(BigNumber(lowestAsk).times(BigNumber(1).plus(BigNumber(this.slippagePercentage).dividedBy(BigNumber(100)))).round(2)); var balance = (BigNumber(this.orderDetails.currencyBalance).minus(BigNumber(this.tradingReserveCurrency))).times(BigNumber(1).minus(BigNumber(this.orderDetails.tradingFee).dividedBy(BigNumber(100)))); - logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); + this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); this.orderDetails.price = lowestAskWithSlippage; this.orderDetails.amount = Number(balance.dividedBy(BigNumber(this.orderDetails.price)).minus(BigNumber(0.005)).round(2)); @@ -102,7 +102,7 @@ tradingagent.prototype.calculateOrder = function(result) { var highestBidWithSlippage = Number(BigNumber(highestBid).times(BigNumber(1).minus(BigNumber(this.slippagePercentage).dividedBy(BigNumber(100)))).round(2)); - logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); + this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); this.orderDetails.price = highestBidWithSlippage; this.orderDetails.amount = Number(BigNumber(this.orderDetails.assetBalance).minus(BigNumber(this.tradingReserveAsset))); @@ -111,31 +111,31 @@ tradingagent.prototype.calculateOrder = function(result) { }; -tradingagent.prototype.placeRealOrder = function() { +agent.prototype.placeRealOrder = function() { if(this.orderDetails.amount <= 0) { - logger.log('Insufficient funds to place an order.'); + this.logger.log('Insufficient funds to place an order.'); } else { - api.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, true, this.processOrder); + this.exchangeapi.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, true, this.processOrder); } }; -tradingagent.prototype.placeSimulatedOrder = function() { +agent.prototype.placeSimulatedOrder = function() { if(this.orderDetails.amount <= 0) { - logger.log('Insufficient funds to place an order.'); + this.logger.log('Insufficient funds to place an order.'); } else { this.orderDetails.order = 'Simulated'; - logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); + this.logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); this.emit('simulatedOrder', this.orderDetails); @@ -143,17 +143,17 @@ tradingagent.prototype.placeSimulatedOrder = function() { }; -tradingagent.prototype.processOrder = function(err, order) { +agent.prototype.processOrder = function(err, order) { if(!order) { - logger.log('Something went wrong when placing the ' + this.orderDetails.orderType + ' order.'); + this.logger.log('Something went wrong when placing the ' + this.orderDetails.orderType + ' order.'); } else { this.orderDetails.order = order.txid; - logger.log('Placed ' + this.orderDetails.orderType + ' order: ' + this.orderDetails.order + ' (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); + this.logger.log('Placed ' + this.orderDetails.orderType + ' order: ' + this.orderDetails.order + ' (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); this.emit('realOrder', this.orderDetails); @@ -161,4 +161,4 @@ tradingagent.prototype.processOrder = function(err, order) { }; -module.exports = tradingagent; +module.exports = agent; diff --git a/tests/dbhealth.js b/tests/dbhealth.js index 2fe0018..e85c198 100644 --- a/tests/dbhealth.js +++ b/tests/dbhealth.js @@ -1,8 +1,15 @@ var _ = require('underscore'); var config = require('../config.js'); -var storage = require('../services/candlestorage.js'); +var loggingservice = require('../services/loggingservice.js'); + +var database = require('../services/db.js'); +var candlestorage = require('../services/candlestorage.js'); var dataprocessor = require('../services/dataprocessor.js'); -var processor = new dataprocessor(config.candleStickSizeMinutes); + +var logger = new loggingservice(config.debug); +var db = new database(config.exchangeSettings, config.mongoConnectionString, logger); +var storage = new candlestorage(db, logger); +var processor = new dataprocessor(storage, logger); processor.on('initialized', function(){ @@ -16,9 +23,9 @@ processor.on('initialized', function(){ _.each(loopArray, function(cs) { if(cs.period !== testPeriod + 60) { - console.log('There is a gap between the following two candlesticks:'); - console.log('Previous: ' + JSON.stringify(previousCS)); - console.log('Current: ' + JSON.stringify(cs)); + logger.log('There is a gap between the following two candlesticks:'); + logger.log('Previous: ' + JSON.stringify(previousCS)); + logger.log('Current: ' + JSON.stringify(cs)); success = false; } @@ -28,9 +35,9 @@ processor.on('initialized', function(){ }); if(success) { - console.log("Database OK!"); + logger.log("Database OK!"); } else { - console.log("Database corrupt/incomplete, empty your database and try collecting historical information again"); + logger.log("Database corrupt/incomplete, empty your database and try collecting historical information again"); } }); diff --git a/services/tools.js b/util/tools.js similarity index 100% rename from services/tools.js rename to util/tools.js From 6f27b1befe76c919e49bcebe70aaa6c0580d21a7 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 25 Aug 2014 20:26:33 +0200 Subject: [PATCH 24/57] v0.8.1 Bug fixes - Fixed an issue in pushservice.js - Caught an error that could occur when submitting orders to Bitstamp --- README.md | 23 ++++++++++++++++------- app.js | 2 +- backtester.js | 2 +- exchanges/bitstamp.js | 10 +++++++++- package.json | 4 ++-- services/pushservice.js | 2 +- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8e3a0b7..ece2943 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ BitBot BitBot is a Crypto-Currency trading bot and backtesting platform that connects to popular Bitcoin exchanges (Bitstamp, Kraken). It is written in javascript and runs on [Node.JS](http://nodejs.org). -# Dependencies +## Dependencies - [Node.JS](http://nodejs.org) - [MongoDB](http://www.mongodb.org/) -# Installation +## Installation Make sure you have the latest Node.JS version and MongoDB installed. @@ -18,7 +18,16 @@ Clone this repository to a folder of your liking and execute the following comma Pay close attention to the log messages of NPM (there shouldn't be any errors). -# Configuration basics +## Upgrading + +- Overwrite your files with the latest versions from this repository +- Compare you config.js file with the config.sample.js file to make sure there aren't any extra settings available +- Run the following command: + + + npm install + +## Configuration basics Copy the config.sample.js file and name it config.js. @@ -62,7 +71,7 @@ By default you will see a lot of debugging information, once you are sure that y These are the minimum required settings you need to get the bot started. All other settings are user preference and should be pretty self explanatory. -# Indicator and CandleStickSize settings +## Indicator and CandleStickSize settings These settings aren't part of the configuration basics as they heavily depend on user preference. @@ -125,7 +134,7 @@ As of version 0.7 BitBot now uses indicators as small plugins. You can create yo For examples on how to use this template, go have a look at one of the existing indicators in the indicators folder. -# Usage +## Usage Execute the following command in the folder where you installed BitBot: @@ -137,12 +146,12 @@ If you would like to simulate trading on your collected data, execute: Remember the backtester simulates your trading strategy on the data you collected. So the longer you keep the bot (app.js) running, the more significant the results of the backtester. -# Profitability +## Profitability The provided trading algorithms are well known and documented on the internet (MADC, PPO). I do not guarantee you will make any profit when using this bot... For better results, consider writing your own algorithm and share it with the community in a pull request :-). -# Donations +## Donations If you enjoyed using BitBot or would like to help me continue development of this bot, consider buying me a beer: (BTC) 1BitBotSYYMAsn6c1AsrxWphWAGkNE6hmQ diff --git a/app.js b/app.js index c93b0a0..85bd860 100644 --- a/app.js +++ b/app.js @@ -172,7 +172,7 @@ var start = function() { //------------------------------AnnounceStart console.log('------------------------------------------'); - console.log('Starting BitBot v0.8.0'); + console.log('Starting BitBot v0.8.1'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index 60dd9d0..bb214dc 100644 --- a/backtester.js +++ b/backtester.js @@ -66,7 +66,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.8.0'); +console.log('Starting BitBot Back-Tester v0.8.1'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 50a8c88..095e1ab 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -164,7 +164,15 @@ exchange.prototype.placeOrder = function(caller, args, type, amount, price, retr if(!err) { - cb(null, {txid: result.id}); + if(!result.error) { + + cb(null, {txid: result.id}); + + } else { + + cb(result.error, null); + + } } else { diff --git a/package.json b/package.json index b55b5d9..973221e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "BitBot", - "version": "0.8.0", + "version": "0.8.1", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { "async": "0.9.0", "bignumber.js": "1.2.1", - "bitstamp-api": "0.1.1", + "bitstamp-api": "0.1.3", "mime": "1.2.11", "moment": "2.4.0", "mongojs": "0.14.0", diff --git a/services/pushservice.js b/services/pushservice.js index 36f1b57..297f5c7 100644 --- a/services/pushservice.js +++ b/services/pushservice.js @@ -44,7 +44,7 @@ pusher.prototype.send = function(title, message, sound, priority) { } this.logger.log('Push notification sent!'); - }); + }.bind(this)); } else { From 366fcf8a4dce67cd65bad26875c8edfac5168135 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 25 Aug 2014 21:06:56 +0200 Subject: [PATCH 25/57] v0.8.2 Require new bitstamp-api package 0.1.5 --- app.js | 2 +- backtester.js | 2 +- package.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 85bd860..aecb42a 100644 --- a/app.js +++ b/app.js @@ -172,7 +172,7 @@ var start = function() { //------------------------------AnnounceStart console.log('------------------------------------------'); - console.log('Starting BitBot v0.8.1'); + console.log('Starting BitBot v0.8.2'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index bb214dc..ba994d6 100644 --- a/backtester.js +++ b/backtester.js @@ -66,7 +66,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.8.1'); +console.log('Starting BitBot Back-Tester v0.8.2'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/package.json b/package.json index 973221e..20a888b 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "BitBot", - "version": "0.8.1", + "version": "0.8.2", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { "async": "0.9.0", "bignumber.js": "1.2.1", - "bitstamp-api": "0.1.3", + "bitstamp-api": "0.1.5", "mime": "1.2.11", "moment": "2.4.0", "mongojs": "0.14.0", From 1de95fcd8c1d2233899fac17c86a0bb314dbe08a Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 25 Aug 2014 21:25:41 +0200 Subject: [PATCH 26/57] Updated README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ece2943..8c8e4e4 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,10 @@ Remember the backtester simulates your trading strategy on the data you collecte The provided trading algorithms are well known and documented on the internet (MADC, PPO). I do not guarantee you will make any profit when using this bot... For better results, consider writing your own algorithm and share it with the community in a pull request :-). +## FAQ +- Q: I often receive invalid nonce / invalid signature errors when using Bitstamp, how do I fix that? +- A: Generate a new API key on the Bitstamp website. + ## Donations If you enjoyed using BitBot or would like to help me continue development of this bot, consider buying me a beer: From ca038029656574d890d2fdf191413663e47dd34f Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 25 Aug 2014 21:34:51 +0200 Subject: [PATCH 27/57] bitstamp-api dependency version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20a888b..fc87c64 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "async": "0.9.0", "bignumber.js": "1.2.1", - "bitstamp-api": "0.1.5", + "bitstamp-api": "0.1.7", "mime": "1.2.11", "moment": "2.4.0", "mongojs": "0.14.0", From cc62fe3c6eaa63f4a1c41193ff16b5ba96bec154 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 26 Aug 2014 20:59:38 +0200 Subject: [PATCH 28/57] v0.8.3 Kraken API handler bug fix --- README.md | 3 +-- app.js | 2 +- backtester.js | 2 +- exchanges/kraken.js | 14 +++++++++----- package.json | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8c8e4e4..b1b1784 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -BitBot -====== +#BitBot BitBot is a Crypto-Currency trading bot and backtesting platform that connects to popular Bitcoin exchanges (Bitstamp, Kraken). It is written in javascript and runs on [Node.JS](http://nodejs.org). diff --git a/app.js b/app.js index aecb42a..a2b6eb0 100644 --- a/app.js +++ b/app.js @@ -172,7 +172,7 @@ var start = function() { //------------------------------AnnounceStart console.log('------------------------------------------'); - console.log('Starting BitBot v0.8.2'); + console.log('Starting BitBot v0.8.3'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index ba994d6..aea65dc 100644 --- a/backtester.js +++ b/backtester.js @@ -66,7 +66,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.8.2'); +console.log('Starting BitBot Back-Tester v0.8.3'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/exchanges/kraken.js b/exchanges/kraken.js index 7f85fe7..47cd62d 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -16,11 +16,11 @@ var exchange = function(currencyPair, apiSettings, cbManager, logger) { }; -exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { return function(err, result) { - var cb = this.cbManager(func, receivedArgs, retryAllowed, handler); + var cb = this.cbManager(caller, receivedArgs, retryAllowed, handler); var parsedError = null; @@ -290,9 +290,9 @@ exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { var wrapper = function() { - this.orderFilled(order, true, function(err, filled) { + this.orderFilled(caller, args, order, retry, function(err, filled) { - if(!filled) { + if(!filled && !err) { var handler = function(err, data) { @@ -314,10 +314,14 @@ exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(caller, args, retry, 'cancelOrder', handler)); - } else { + } else if(filled && !err) { cb(null, false); + } else { + + cb(err, null); + } }.bind(this)); diff --git a/package.json b/package.json index fc87c64..cb5dba3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.8.2", + "version": "0.8.3", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { From 4f2931ef931bf276d166834fdcae294ff75809a7 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Thu, 28 Aug 2014 20:47:16 +0200 Subject: [PATCH 29/57] v0.8.4 Simplified exchange interface code - Simplified exchange interface code - Added cancelorder.js test module - Fixed a bug in order monitoring that wasn't fixed in previous release --- app.js | 2 +- backtester.js | 2 +- exchanges/bitstamp.js | 54 ++++++++++++++++---------- exchanges/kraken.js | 84 ++++++++++++++++++++++++++--------------- package.json | 2 +- services/exchangeapi.js | 48 ++++++----------------- tests/cancelorder.js | 54 ++++++++++++++++++++++++++ 7 files changed, 156 insertions(+), 90 deletions(-) create mode 100644 tests/cancelorder.js diff --git a/app.js b/app.js index a2b6eb0..997ddb0 100644 --- a/app.js +++ b/app.js @@ -172,7 +172,7 @@ var start = function() { //------------------------------AnnounceStart console.log('------------------------------------------'); - console.log('Starting BitBot v0.8.3'); + console.log('Starting BitBot v0.8.4'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index aea65dc..b0823cc 100644 --- a/backtester.js +++ b/backtester.js @@ -66,7 +66,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.8.3'); +console.log('Starting BitBot Back-Tester v0.8.4'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 095e1ab..dcf4e83 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -1,7 +1,7 @@ var _ = require('underscore'); var Bitstamp = require('bitstamp-api'); -var exchange = function(currencyPair, apiSettings, cbManager, logger) { +var exchange = function(currencyPair, apiSettings, cbManager, q, logger) { this.currencyPair = currencyPair; @@ -9,6 +9,8 @@ var exchange = function(currencyPair, apiSettings, cbManager, logger) { this.cbManager = cbManager; + this.q = q; + this.logger = logger; _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); @@ -51,7 +53,9 @@ exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, cal }; -exchange.prototype.getTrades = function(caller, args, retry, cb) { +exchange.prototype.getTrades = function(retry, cb) { + + var args = arguments; var wrapper = function() { @@ -79,15 +83,17 @@ exchange.prototype.getTrades = function(caller, args, retry, cb) { }; - this.bitstamp.transactions({time: 'hour'}, this.errorHandler(caller, args, retry, 'getTrades', handler)); + this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.getBalance = function(caller, args, retry, cb) { +exchange.prototype.getBalance = function(retry, cb) { + + var args = arguments; var wrapper = function() { @@ -110,15 +116,17 @@ exchange.prototype.getBalance = function(caller, args, retry, cb) { }; - this.bitstamp.balance(this.errorHandler(caller, args, retry, 'getBalance', handler)); + this.bitstamp.balance(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.getOrderBook = function(caller, args, retry, cb) { +exchange.prototype.getOrderBook = function(retry, cb) { + + var args = arguments; var wrapper = function () { @@ -146,15 +154,17 @@ exchange.prototype.getOrderBook = function(caller, args, retry, cb) { }; - this.bitstamp.order_book(1, this.errorHandler(caller, args, retry, 'getOrderBook', handler)); + this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.placeOrder = function(caller, args, type, amount, price, retry, cb) { +exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { + + var args = arguments; var wrapper = function() { @@ -184,11 +194,11 @@ exchange.prototype.placeOrder = function(caller, args, type, amount, price, retr if(type === 'buy') { - this.bitstamp.buy(amount, price, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else if (type === 'sell') { - this.bitstamp.sell(amount, price, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else { @@ -198,11 +208,13 @@ exchange.prototype.placeOrder = function(caller, args, type, amount, price, retr }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.orderFilled = function(caller, args, order, retry, cb) { +exchange.prototype.orderFilled = function(order, retry, cb) { + + var args = arguments; var wrapper = function() { @@ -234,15 +246,17 @@ exchange.prototype.orderFilled = function(caller, args, order, retry, cb) { }; - this.bitstamp.open_orders(this.errorHandler(caller, args, retry, 'orderFilled', handler)); + this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { +exchange.prototype.cancelOrder = function(order, retry, cb) { + + var args = arguments; var wrapper = function() { @@ -264,11 +278,11 @@ exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { }; - this.bitstamp.cancel_order(order,this.errorHandler(caller, args, retry, 'cancelOrder', handler)); + this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; diff --git a/exchanges/kraken.js b/exchanges/kraken.js index 47cd62d..6af2550 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -1,7 +1,7 @@ var _ = require('underscore'); var Kraken = require('kraken-api'); -var exchange = function(currencyPair, apiSettings, cbManager, logger) { +var exchange = function(currencyPair, apiSettings, cbManager, q, logger) { this.currencyPair = currencyPair; @@ -9,6 +9,8 @@ var exchange = function(currencyPair, apiSettings, cbManager, logger) { this.cbManager = cbManager; + this.q = q; + this.logger = logger; _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); @@ -61,7 +63,9 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c }; -exchange.prototype.getTrades = function(caller, args, retry, cb) { +exchange.prototype.getTrades = function(retry, cb) { + + var args = arguments; var wrapper = function() { @@ -93,15 +97,17 @@ exchange.prototype.getTrades = function(caller, args, retry, cb) { }; - this.kraken.api('Trades', {"pair": pair}, this.errorHandler(caller, args, retry, 'getTrades', handler)); + this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.getBalance = function(caller, args, retry, cb) { +exchange.prototype.getBalance = function(retry, cb) { + + var args = arguments; var wrapper = function() { @@ -130,23 +136,31 @@ exchange.prototype.getBalance = function(caller, args, retry, cb) { currencyValue = 0; } - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(caller, args, retry, 'getBalance', function(err, data) { + var secondWrapper = function() { - if(!err) { + var secondHandler = function(err, data) { - var fee = parseFloat(_.find(data.result.fees, function(value, key) { - return key === pair; - }).fee); + if(!err) { - cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + var fee = parseFloat(_.find(data.result.fees, function(value, key) { + return key === pair; + }).fee); - } else { + cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); - cb(err, null); + } else { - } + cb(err, null); - })); + } + + }.bind(this); + + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, retry, 'getBalance', secondHandler)); + + }.bind(this); + + this.q.push(secondWrapper); } else { @@ -156,15 +170,17 @@ exchange.prototype.getBalance = function(caller, args, retry, cb) { }.bind(this); - this.kraken.api('Balance', {}, this.errorHandler(caller, args, retry, 'getBalance', handler)); + this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.getOrderBook = function(caller, args, retry, cb) { +exchange.prototype.getOrderBook = function(retry, cb) { + + var args = arguments; var wrapper = function () { @@ -198,15 +214,17 @@ exchange.prototype.getOrderBook = function(caller, args, retry, cb) { }; - this.kraken.api('Depth', {"pair": pair}, this.errorHandler(caller, args, retry, 'getOrderBook', handler)); + this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.placeOrder = function(caller, args, type, amount, price, retry, cb) { +exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { + + var args = arguments; var wrapper = function() { @@ -228,11 +246,11 @@ exchange.prototype.placeOrder = function(caller, args, type, amount, price, retr if(type === 'buy') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else if (type === 'sell') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(caller, args, retry, 'placeOrder', handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); } else { @@ -242,11 +260,13 @@ exchange.prototype.placeOrder = function(caller, args, type, amount, price, retr }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.orderFilled = function(caller, args, order, retry, cb) { +exchange.prototype.orderFilled = function(order, retry, cb) { + + var args = arguments; var wrapper = function() { @@ -278,19 +298,21 @@ exchange.prototype.orderFilled = function(caller, args, order, retry, cb) { }; - this.kraken.api('OpenOrders', {}, this.errorHandler(caller, args, retry, 'orderFilled', handler)); + this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); }.bind(this); - return wrapper; + this.q.push(wrapper); }; -exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { +exchange.prototype.cancelOrder = function(order, retry, cb) { + + var args = arguments; var wrapper = function() { - this.orderFilled(caller, args, order, retry, function(err, filled) { + this.orderFilled(order, retry, function(err, filled) { if(!filled && !err) { @@ -312,7 +334,7 @@ exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { }; - this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(caller, args, retry, 'cancelOrder', handler)); + this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); } else if(filled && !err) { @@ -328,7 +350,7 @@ exchange.prototype.cancelOrder = function(caller, args, order, retry, cb) { }.bind(this); - return wrapper; + this.q.push(wrapper); }; diff --git a/package.json b/package.json index cb5dba3..94f1e2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.8.3", + "version": "0.8.4", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/exchangeapi.js b/services/exchangeapi.js index 1ef0e80..1ff049b 100644 --- a/services/exchangeapi.js +++ b/services/exchangeapi.js @@ -8,20 +8,20 @@ var api = function(exchangeSettings, apiSettings, logger) { this.currencyPair = exchangeSettings.currencyPair; this.logger = logger; + this.q = async.queue(function (task, callback) { + task(); + setTimeout(callback,1000); + }, 1); + if(fs.existsSync('./exchanges/' + this.exchange + '.js')) { var Exchange = require('../exchanges/' + this.exchange + '.js'); - this.selectedExchange = new Exchange(this.currencyPair, apiSettings[this.exchange], this.cbManager.bind(this), logger); + this.selectedExchange = new Exchange(this.currencyPair, apiSettings[this.exchange], this.cbManager.bind(this), this.q, logger); } else { var err = new Error('Wrong exchange chosen. This exchange doesn\'t exist.'); this.logger.error(err.stack); process.exit(); } - this.q = async.queue(function (task, callback) { - task(); - setTimeout(callback,1000); - }, 1); - _.bindAll(this, 'retry', 'cbManager', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); }; @@ -74,61 +74,37 @@ api.prototype.cbManager = function(method, receivedArgs, retryAllowed, cb) { api.prototype.getTrades = function(retry, cb) { - var args = arguments; - - var wrapper = this.selectedExchange.getTrades(this.getTrades, args, retry, cb); - - this.q.push(wrapper); + this.selectedExchange.getTrades(retry, cb); }; api.prototype.getBalance = function(retry, cb) { - var args = arguments; - - var wrapper = this.selectedExchange.getBalance(this.getBalance, args, retry, cb); - - this.q.push(wrapper); + this.selectedExchange.getBalance(retry, cb); }; api.prototype.getOrderBook = function(retry, cb) { - var args = arguments; - - var wrapper = this.selectedExchange.getOrderBook(this.getOrderBook, args, retry, cb); - - this.q.push(wrapper); + this.selectedExchange.getOrderBook(retry, cb); }; api.prototype.placeOrder = function(type, amount, price, retry, cb) { - var args = arguments; - - var wrapper = this.selectedExchange.placeOrder(this.placeOrder, args, type, amount, price, retry, cb); - - this.q.push(wrapper); + this.selectedExchange.placeOrder(type, amount, price, retry, cb); }; api.prototype.orderFilled = function(order, retry, cb) { - var args = arguments; - - var wrapper = this.selectedExchange.orderFilled(this.orderFilled, args, order, retry, cb); - - this.q.push(wrapper); + this.selectedExchange.orderFilled(order, retry, cb); }; api.prototype.cancelOrder = function(order, retry, cb) { - var args = arguments; - - var wrapper = this.selectedExchange.cancelOrder(this.cancelOrder, args, order, retry, cb); - - this.q.push(wrapper); + this.selectedExchange.cancelOrder(order, retry, cb); }; diff --git a/tests/cancelorder.js b/tests/cancelorder.js new file mode 100644 index 0000000..9e44000 --- /dev/null +++ b/tests/cancelorder.js @@ -0,0 +1,54 @@ +//------------------------------Config +var config = require('../config.js'); +//------------------------------Config + +var exchangeapiservice = require('../services/exchangeapi.js'); +var loggingservice = require('../services/loggingservice.js'); +var _ = require('underscore'); + +var logger = new loggingservice(config.debug); +var api = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); + +api.getBalance(true, function(err, result) { + + var processOrder = function(err, result) { + + setTimeout(function() { + + api.cancelOrder(result.txid, true, function(err, result) { + + logger.log('Cancelled: ' + result); + + if(result) { + + logger.log('Test completed successfully.'); + + } else { + + logger.log('Test failed'); + + } + + }); + + }, 30000); + + logger.log('Order placed, waiting 30 seconds before cancelling.'); + + }; + + if(result.currencyAvailable > 5) { + + api.placeOrder('buy', 100000, 0.00005, true, processOrder); + + } else if(result.assetAvailable > 0.00005) { + + api.placeOrder('sell', 0.00005, 100000, true, processOrder); + + } else { + + logger.log('Insufficient funds to run test.'); + + } + +}); From 1b27f836c4e83c09a3e1109640b3dc28fcbf7760 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Wed, 1 Oct 2014 20:13:15 +0200 Subject: [PATCH 30/57] v0.8.5 Stability & troubleshooting improvements - Application now has an extra dependency: winston - Logging and debugging information is now logged to rotating log files. - Debugging information is no longer polluting the console logs. --- app.js | 2 +- backtester.js | 2 +- exchanges/bitstamp.js | 51 ++++++++++++++++++-------- exchanges/kraken.js | 50 ++++++++++++++++++-------- exchanges/template.js | 73 +++++++++++++++++++++++++------------- package.json | 9 ++--- services/dataprocessor.js | 4 +-- services/dataretriever.js | 2 ++ services/exchangeapi.js | 56 ++--------------------------- services/loggingservice.js | 45 ++++++++++++++++++----- 10 files changed, 171 insertions(+), 123 deletions(-) diff --git a/app.js b/app.js index 997ddb0..ba96aaa 100644 --- a/app.js +++ b/app.js @@ -172,7 +172,7 @@ var start = function() { //------------------------------AnnounceStart console.log('------------------------------------------'); - console.log('Starting BitBot v0.8.4'); + console.log('Starting BitBot v0.8.5'); console.log('Real Trading Enabled = ' + config.tradingEnabled); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index b0823cc..64854bc 100644 --- a/backtester.js +++ b/backtester.js @@ -66,7 +66,7 @@ var endDate; //------------------------------AnnounceStart console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.8.4'); +console.log('Starting BitBot Back-Tester v0.8.5'); console.log('Working Dir = ' + process.cwd()); console.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index dcf4e83..dcbc4c0 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -1,27 +1,49 @@ var _ = require('underscore'); +var async = require('async'); var Bitstamp = require('bitstamp-api'); -var exchange = function(currencyPair, apiSettings, cbManager, q, logger) { +var exchange = function(currencyPair, apiSettings, logger) { this.currencyPair = currencyPair; this.bitstamp = new Bitstamp(apiSettings.apiKey, apiSettings.secret, apiSettings.clientId); - this.cbManager = cbManager; - - this.q = q; + this.q = async.queue(function (task, callback) { + this.logger.debug('Added ' + task.name + ' API call to the queue.'); + this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); + task.func(); + setTimeout(callback,1000); + }.bind(this), 1); this.logger = logger; - _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + +}; + +exchange.prototype.retry = function(method, args) { + + var self = this; + + // make sure the callback (and any other fn) + // is bound to api + _.each(args, function(arg, i) { + if(_.isFunction(arg)) + args[i] = _.bind(arg, self); + }); + + // run the failed method again with the same + // arguments after wait + + setTimeout(function() { method.apply(self, args); }, 1000*15); }; -exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { return function(err, result) { - var cb = this.cbManager(func, receivedArgs, retryAllowed, handler); + var args = _.toArray(receivedArgs); var parsedError = null; @@ -38,6 +60,7 @@ exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, cal if(retryAllowed) { this.logger.error('Retrying in 15 seconds!'); + return this.retry(caller, args); } } else { @@ -47,7 +70,7 @@ exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, cal } - cb(parsedError, result); + handler(parsedError, result); }.bind(this); @@ -87,7 +110,7 @@ exchange.prototype.getTrades = function(retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'getTrades', func: wrapper}); }; @@ -120,7 +143,7 @@ exchange.prototype.getBalance = function(retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'getBalance', func: wrapper}); }; @@ -158,7 +181,7 @@ exchange.prototype.getOrderBook = function(retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'getOrderBook', func: wrapper}); }; @@ -208,7 +231,7 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'placeOrder', func: wrapper}); }; @@ -250,7 +273,7 @@ exchange.prototype.orderFilled = function(order, retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'orderFilled', func: wrapper}); }; @@ -282,7 +305,7 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'cancelOrder', func: wrapper}); }; diff --git a/exchanges/kraken.js b/exchanges/kraken.js index 6af2550..b9a6204 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -1,28 +1,49 @@ var _ = require('underscore'); +var async = require('async'); var Kraken = require('kraken-api'); -var exchange = function(currencyPair, apiSettings, cbManager, q, logger) { +var exchange = function(currencyPair, apiSettings, logger) { this.currencyPair = currencyPair; this.kraken = new Kraken(apiSettings.apiKey, apiSettings.secret); - this.cbManager = cbManager; - - this.q = q; + this.q = async.queue(function (task, callback) { + this.logger.debug('Added ' + task.name + ' API call to the queue.'); + this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); + task.func(); + setTimeout(callback,1000); + }.bind(this), 1); this.logger = logger; - _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); }; +exchange.prototype.retry = function(method, args) { + + var self = this; + + // make sure the callback (and any other fn) + // is bound to api + _.each(args, function(arg, i) { + if(_.isFunction(arg)) + args[i] = _.bind(arg, self); + }); + + // run the failed method again with the same + // arguments after wait + + setTimeout(function() { method.apply(self, args); }, 1000*15); + +}; exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { return function(err, result) { - var cb = this.cbManager(caller, receivedArgs, retryAllowed, handler); + var args = _.toArray(receivedArgs); var parsedError = null; @@ -46,6 +67,7 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c if(retryAllowed) { this.logger.error('Retrying in 15 seconds!'); + return this.retry(caller, args); } } @@ -57,7 +79,7 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c } - cb(parsedError, result); + handler(parsedError, result); }.bind(this); @@ -101,7 +123,7 @@ exchange.prototype.getTrades = function(retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'getTrades', func: wrapper}); }; @@ -160,7 +182,7 @@ exchange.prototype.getBalance = function(retry, cb) { }.bind(this); - this.q.push(secondWrapper); + this.q.push({name: 'TradeVolume', func: secondWrapper}); } else { @@ -174,7 +196,7 @@ exchange.prototype.getBalance = function(retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'getBalance', func: wrapper}); }; @@ -218,7 +240,7 @@ exchange.prototype.getOrderBook = function(retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'getOrderBook', func: wrapper}); }; @@ -260,7 +282,7 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'placeOrder', func: wrapper}); }; @@ -302,7 +324,7 @@ exchange.prototype.orderFilled = function(order, retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'orderFilled', func: wrapper}); }; @@ -350,7 +372,7 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { }.bind(this); - this.q.push(wrapper); + this.q.push({name: 'cancelOrder', func: wrapper}); }; diff --git a/exchanges/template.js b/exchanges/template.js index 730acfb..1dc9ac6 100644 --- a/exchanges/template.js +++ b/exchanges/template.js @@ -4,26 +4,50 @@ process.exit(); //-------------------- REMOVE THIS BLOCK var _ = require('underscore'); +var async = require('async'); -var exchange = function(currencyPair, apiSettings, cbManager, logger) { +var exchange = function(currencyPair, apiSettings, logger) { this.currencyPair = currencyPair; // intialize your API with it's apiSettings here - this.cbManager = cbManager; + this.q = async.queue(function (task, callback) { + this.logger.debug('Added ' + task.name + ' API call to the queue.'); + this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); + task.func(); + setTimeout(callback,1000); + }.bind(this), 1); this.logger = logger; - _.bindAll(this, 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); }; -exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.retry = function(method, args) { + + var self = this; + + // make sure the callback (and any other fn) + // is bound to api + _.each(args, function(arg, i) { + if(_.isFunction(arg)) + args[i] = _.bind(arg, self); + }); + + // run the failed method again with the same + // arguments after wait + + setTimeout(function() { method.apply(self, args); }, 1000*15); + +}; + +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { return function(err, result) { - var cb = this.cbManager(func, receivedArgs, retryAllowed, callerName, handler); + var args = _.toArray(receivedArgs); var parsedError = null; @@ -40,6 +64,7 @@ exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, cal if(retryAllowed) { this.logger.error('Retrying in 15 seconds!'); + return this.retry(caller, args); } } else { @@ -49,13 +74,13 @@ exchange.prototype.errorHandler = function(func, receivedArgs, retryAllowed, cal } - cb(parsedError, result); + handler(parsedError, result); }.bind(this); }; -exchange.prototype.getTrades = function(caller, retry, cb) { +exchange.prototype.getTrades = function(retry, cb) { var args = arguments; @@ -68,15 +93,15 @@ exchange.prototype.getTrades = function(caller, retry, cb) { }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(caller, args, retry, 'getTrades', handler); + this.errorHandler(this.getTrades, args, retry, 'getTrades', handler); }.bind(this); - return wrapper; + this.q.push({name: 'getTrades', func: wrapper}); }; -exchange.prototype.getBalance = function(caller, retry, cb) { +exchange.prototype.getBalance = function(retry, cb) { var args = arguments; @@ -89,15 +114,15 @@ exchange.prototype.getBalance = function(caller, retry, cb) { }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(caller, args, retry, 'getBalance', handler); + this.errorHandler(this.getBalance, args, retry, 'getBalance', handler); }.bind(this); - return wrapper; + this.q.push({name: 'getBalance', func: wrapper}); }; -exchange.prototype.getOrderBook = function(caller, retry, cb) { +exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; @@ -110,15 +135,15 @@ exchange.prototype.getOrderBook = function(caller, retry, cb) { }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(caller, args, retry, 'getOrderBook', handler); + this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler); }.bind(this); - return wrapper; + this.q.push({name: 'getOrderBook', func: wrapper}); }; -exchange.prototype.placeOrder = function(caller, type, amount, price, retry, cb) { +exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; @@ -131,15 +156,15 @@ exchange.prototype.placeOrder = function(caller, type, amount, price, retry, cb) }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(caller, args, retry, 'placeOrder', handler); + this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler); }.bind(this); - return wrapper; + this.q.push({name: 'placeOrder', func: wrapper}); }; -exchange.prototype.orderFilled = function(caller, order, retry, cb) { +exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; @@ -152,15 +177,15 @@ exchange.prototype.orderFilled = function(caller, order, retry, cb) { }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(caller, args, retry, 'orderFilled', handler); + this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler); }.bind(this); - return wrapper; + this.q.push({name: 'orderFilled', func: wrapper}); }; -exchange.prototype.cancelOrder = function(caller, order, retry, cb) { +exchange.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; @@ -173,11 +198,11 @@ exchange.prototype.cancelOrder = function(caller, order, retry, cb) { }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(caller, args, retry, 'cancelOrder', handler); + this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler); }.bind(this); - return wrapper; + this.q.push({name: 'cancelOrder', func: wrapper}); }; diff --git a/package.json b/package.json index 94f1e2d..275cc19 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,20 @@ { "name": "BitBot", - "version": "0.8.4", + "version": "0.8.5", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { "async": "0.9.0", "bignumber.js": "1.2.1", - "bitstamp-api": "0.1.7", + "bitstamp-api": "0.1.8", "mime": "1.2.11", "moment": "2.4.0", - "mongojs": "0.14.0", + "mongojs": "0.14.2", "pushover-notifications": "0.1.5", "underscore": "1.6.0", "ws": "0.4.31", - "kraken-api": "0.1.5" + "kraken-api": "0.1.5", + "winston": "0.8.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/services/dataprocessor.js b/services/dataprocessor.js index eb51a3c..0c0d374 100644 --- a/services/dataprocessor.js +++ b/services/dataprocessor.js @@ -160,7 +160,7 @@ processor.prototype.processInitialLoad = function(err, result) { if(err) { - var parsedError = err; + var parsedError = JSON.stringify(err); if(err.stack) { parsedError = err.stack; @@ -185,7 +185,7 @@ processor.prototype.processUpdate = function(err, result) { if(err) { - var parsedError = err; + var parsedError = JSON.stringify(err); if(err.stack) { parsedError = err.stack; diff --git a/services/dataretriever.js b/services/dataretriever.js index fd201ed..7dbdb37 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -43,9 +43,11 @@ downloader.prototype.start = function() { this.logger.log('Downloader started!'); + this.logger.debug('Retrieving new trades from exchange API.'); this.exchangeapi.getTrades(false, this.processTrades); this.downloadInterval = setInterval(function(){ + this.logger.debug('Retrieving new trades from exchange API.'); this.exchangeapi.getTrades(false, this.processTrades); }.bind(this),1000 * this.refreshInterval); diff --git a/services/exchangeapi.js b/services/exchangeapi.js index 1ff049b..d11c54c 100644 --- a/services/exchangeapi.js +++ b/services/exchangeapi.js @@ -1,5 +1,4 @@ var _ = require('underscore'); -var async = require('async'); var fs = require('fs'); var api = function(exchangeSettings, apiSettings, logger) { @@ -8,67 +7,16 @@ var api = function(exchangeSettings, apiSettings, logger) { this.currencyPair = exchangeSettings.currencyPair; this.logger = logger; - this.q = async.queue(function (task, callback) { - task(); - setTimeout(callback,1000); - }, 1); - if(fs.existsSync('./exchanges/' + this.exchange + '.js')) { var Exchange = require('../exchanges/' + this.exchange + '.js'); - this.selectedExchange = new Exchange(this.currencyPair, apiSettings[this.exchange], this.cbManager.bind(this), this.q, logger); + this.selectedExchange = new Exchange(this.currencyPair, apiSettings[this.exchange], logger); } else { var err = new Error('Wrong exchange chosen. This exchange doesn\'t exist.'); this.logger.error(err.stack); process.exit(); } - _.bindAll(this, 'retry', 'cbManager', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); - -}; - -api.prototype.retry = function(method, args) { - - var self = this; - - // make sure the callback (and any other fn) - // is bound to api - _.each(args, function(arg, i) { - if(_.isFunction(arg)) - args[i] = _.bind(arg, self); - }); - - // run the failed method again with the same - // arguments after wait - - setTimeout(function() { method.apply(self, args); }, 1000*15); - -}; - -api.prototype.cbManager = function(method, receivedArgs, retryAllowed, cb) { - - var args = _.toArray(receivedArgs); - - return function(err, result) { - - if(err) { - - if(retryAllowed) { - - return this.retry(method, args); - - } else { - - return cb(err, null); - - } - - } else { - - cb(null, result); - - } - - }.bind(this); + _.bindAll(this, 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); }; diff --git a/services/loggingservice.js b/services/loggingservice.js index 40dd169..ed2a3ad 100644 --- a/services/loggingservice.js +++ b/services/loggingservice.js @@ -1,19 +1,50 @@ var moment = require('moment'); var _ = require('underscore'); +var winston = require('winston'); +var fs = require('fs'); var logger = function(debug) { this.debugEnabled = debug; + if (!fs.existsSync('logs')) { + fs.mkdirSync('logs'); + } + + var myCustomLevels = { + levels: { + DEBUG: 0, + INFO: 1, + ERROR: 2 + } + }; + + var now = function() { + var format = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); + return '[' + format + ']'; + }; + + this.logger = new (winston.Logger)({ + levels: myCustomLevels.levels, + transports: [ + new (winston.transports.Console)({ + 'timestamp': now, + level: 'INFO' }), + new (winston.transports.DailyRotateFile)({ + 'timestamp': now, + datePattern: '_dd-MM-yyyy.log', + filename: 'logs/debug', + level: 'DEBUG'}) + ] + }); + _.bindAll(this, 'log', 'debug', 'error'); }; logger.prototype.log = function(message) { - var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - - console.log('[' + now + '] (INFO) ' + message); + this.logger.log('INFO', message); }; @@ -21,9 +52,7 @@ logger.prototype.debug = function(message) { if(this.debugEnabled) { - var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - - console.log('[' + now + '] (DEBUG) ' + message); + this.logger.log('DEBUG', message); } @@ -31,9 +60,7 @@ logger.prototype.debug = function(message) { logger.prototype.error = function(message) { - var now = moment(new Date()).format('DD-MM-YYYY HH:mm:ss'); - - console.log('[' + now + '] (ERROR) ' + message); + this.logger.log('ERROR', message); }; From eaea609ec230d5e207083484b732d5a76564c9fb Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 21 Oct 2014 20:27:34 +0200 Subject: [PATCH 31/57] v0.9.0 Database Layer Rewrite - Database Layer Rewrite - Performance Enhancements --- README.md | 2 +- app.js | 40 ++-- backtester.js | 322 +++++++++++++++------------- config.sample.js | 2 +- indicators/MACD.js | 18 +- indicators/PPO.js | 18 +- indicators/PSAR.js | 12 +- indicators/template.js | 2 +- package.json | 10 +- services/candleaggregator.js | 33 +-- services/candlestorage.js | 372 --------------------------------- services/dataprocessor.js | 192 ++++++++--------- services/db.js | 188 ----------------- services/loggingservice.js | 4 +- services/ordermonitor.js | 1 - services/pricemonitor.js | 49 +++-- services/profitreporter.js | 26 +-- services/storage.js | 394 +++++++++++++++++++++++++++++++++++ services/tradingadvisor.js | 10 +- services/tradingagent.js | 21 +- tests/cancelorder.js | 2 +- tests/dbhealth.js | 14 +- util/tools.js | 18 +- 23 files changed, 818 insertions(+), 932 deletions(-) delete mode 100644 services/candlestorage.js delete mode 100644 services/db.js create mode 100644 services/storage.js diff --git a/README.md b/README.md index b1b1784..c47c587 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ As of version 0.7 BitBot now uses indicators as small plugins. You can create yo //-------------------- REMOVE THIS BLOCK var _ = require('underscore'); - var BigNumber = require('bignumber.js'); + var tools = require('../util/tools.js'); var indicator = function(options) { diff --git a/app.js b/app.js index ba96aaa..e5bd079 100644 --- a/app.js +++ b/app.js @@ -1,8 +1,7 @@ var _ = require('underscore'); var loggingservice = require('./services/loggingservice.js'); -var database = require('./services/db.js'); -var candlestorage = require('./services/candlestorage.js'); +var storageservice = require('./services/storage.js'); var exchangeapiservice = require('./services/exchangeapi.js'); var dataretriever = require('./services/dataretriever.js'); var dataprocessor = require('./services/dataprocessor.js'); @@ -19,9 +18,8 @@ var config = require('./config.js'); //------------------------------Config //------------------------------IntializeModules -var logger = new loggingservice(config.debug); -var db = new database(config.exchangeSettings, config.mongoConnectionString, logger); -var storage = new candlestorage(db, logger); +var logger = new loggingservice('app', config.debug); +var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); var processor = new dataprocessor(storage, logger); @@ -30,7 +28,7 @@ var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSiz var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings, storage, exchangeapi, logger); var pusher = new pushservice(config.pushOver, logger); var monitor = new ordermonitor(exchangeapi, logger); -var reporter = new profitreporter(config.exchangeSettings.currencyPair, db, exchangeapi, logger); +var reporter = new profitreporter(config.exchangeSettings.currencyPair, storage, exchangeapi, logger); var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes, storage, logger); //------------------------------IntializeModules @@ -40,12 +38,6 @@ retriever.on('update', function(ticks){ }); -processor.on('initialized', function(){ - - retriever.start(); - -}); - processor.on('initialDBWrite', function(){ reporter.start(config.resetInitialBalances); @@ -70,11 +62,17 @@ aggregator.on('update', function(cs){ if(config.stoplossSettings.enabled) { - pricemon.update(cs); + pricemon.update(cs, function(err) { - } + advisor.update(cs, false); - advisor.update(cs, false); + }); + + } else { + + advisor.update(cs, false); + + } }); @@ -171,14 +169,14 @@ reporter.on('report', function(report){ var start = function() { //------------------------------AnnounceStart - console.log('------------------------------------------'); - console.log('Starting BitBot v0.8.5'); - console.log('Real Trading Enabled = ' + config.tradingEnabled); - console.log('Working Dir = ' + process.cwd()); - console.log('------------------------------------------'); + logger.log('------------------------------------------'); + logger.log('Starting BitBot v0.9.0'); + logger.log('Real Trading Enabled = ' + config.tradingEnabled); + logger.log('Working Dir = ' + process.cwd()); + logger.log('------------------------------------------'); //------------------------------AnnounceStart - processor.initialize(); + retriever.start(); }; diff --git a/backtester.js b/backtester.js index 64854bc..07c784d 100644 --- a/backtester.js +++ b/backtester.js @@ -1,10 +1,10 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('./util/tools.js'); var moment = require('moment'); +var async = require('async'); var loggingservice = require('./services/loggingservice.js'); -var database = require('./services/db.js'); -var candlestorage = require('./services/candlestorage.js'); +var storageservice = require('./services/storage.js'); var exchangeapiservice = require('./services/exchangeapi.js'); var dataprocessor = require('./services/dataprocessor.js'); var tradingadvisor = require('./services/tradingadvisor.js'); @@ -15,9 +15,8 @@ var config = require('./config.js'); //------------------------------Config //------------------------------IntializeModules -var logger = new loggingservice(config.debug); -var db = new database(config.exchangeSettings, config.mongoConnectionString, logger); -var storage = new candlestorage(db, logger); +var logger = new loggingservice('backtester', config.debug); +var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); var processor = new dataprocessor(storage, logger); var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, true, storage, logger); @@ -46,6 +45,7 @@ var totalFeeCosts = 0; var totalFeeCostsPercentage = 0; var transactions = 0; var slTransactions = 0; +var latestCandlePeriod; var lastClose = 0; var lastClosePlusSlippage = 0; var lastCloseMinusSlippage = 0; @@ -65,10 +65,10 @@ var endDate; //------------------------------IntializeVariables //------------------------------AnnounceStart -console.log('------------------------------------------'); -console.log('Starting BitBot Back-Tester v0.8.5'); -console.log('Working Dir = ' + process.cwd()); -console.log('------------------------------------------'); +logger.log('------------------------------------------'); +logger.log('Starting BitBot Back-Tester v0.9.0'); +logger.log('Working Dir = ' + process.cwd()); +logger.log('------------------------------------------'); //------------------------------AnnounceStart var createOrder = function(type, stopLoss) { @@ -77,174 +77,214 @@ var createOrder = function(type, stopLoss) { if(type === 'buy' && USDBalance !== 0) { - entryUSD = USDBalance; + entryUSD = USDBalance; - usableBalance = Number(BigNumber(USDBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + usableBalance = USDBalance * (1 - (transactionFee / 100)); - lastClosePlusSlippage = Number(BigNumber(lastClose).times(BigNumber(1).plus(BigNumber(slippagePercentage).dividedBy(BigNumber(100)))).round(2)); + lastClosePlusSlippage = tools.round(lastClose * (1 + (slippagePercentage / 100)), 2); - totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance)).round(2)); + totalTradedVolume = tools.round(totalTradedVolume + usableBalance, 2); - totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(USDBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).round(2))); + totalFeeCosts = tools.round(totalFeeCosts + (USDBalance * (transactionFee / 100)), 2); - BTCBalance = Number(BigNumber(BTCBalance).plus(BigNumber(usableBalance).dividedBy(BigNumber(lastClosePlusSlippage)).round(2))); - USDBalance = 0; + BTCBalance = tools.round(BTCBalance + (usableBalance / lastClosePlusSlippage), 2); + USDBalance = 0; - var newUSDBalance = Number(BigNumber(BTCBalance).times(BigNumber(lastClosePlusSlippage)).round(2)); + var newUSDBalance = tools.round(BTCBalance * lastClosePlusSlippage, 2); - if(newUSDBalance > highestUSDValue) { - highestUSDValue = newUSDBalance; - } else if(newUSDBalance < lowestUSDValue) { - lowestUSDValue = newUSDBalance; - } + if(newUSDBalance > highestUSDValue) { + highestUSDValue = newUSDBalance; + } else if(newUSDBalance < lowestUSDValue) { + lowestUSDValue = newUSDBalance; + } - transactions += 1; + transactions += 1; - if(stopLoss) { - slTransactions += 1; - logger.log('Stop loss Triggered an order:'); - } + if(stopLoss) { + slTransactions += 1; + logger.log('Stop loss Triggered an order:'); + } - logger.log('Placed buy order ' + BTCBalance + ' @ ' + lastClosePlusSlippage); + logger.log(new Date(latestCandlePeriod * 1000) + ' Placed buy order ' + BTCBalance + ' @ ' + lastClosePlusSlippage); - pricemon.setPosition('bought', lastClosePlusSlippage); - advisor.setPosition({pos: 'bought', price: lastClosePlusSlippage}); + pricemon.setPosition('bought', lastClosePlusSlippage); + advisor.setPosition({pos: 'bought', price: lastClosePlusSlippage}); } else if(type === 'sell' && BTCBalance !== 0) { - usableBalance = Number(BigNumber(BTCBalance).times(BigNumber(1).minus(BigNumber(transactionFee).dividedBy(BigNumber(100))))); + usableBalance = BTCBalance * (1 - (transactionFee / 100)); - lastCloseMinusSlippage = Number(BigNumber(lastClose).times(BigNumber(1).minus(BigNumber(slippagePercentage).dividedBy(BigNumber(100)))).round(2)); + lastCloseMinusSlippage = tools.round(lastClose * (1 - (slippagePercentage / 100)), 2); - totalTradedVolume = Number(BigNumber(totalTradedVolume).plus(BigNumber(usableBalance).times(BigNumber(lastCloseMinusSlippage))).round(2)); + totalTradedVolume = tools.round(totalTradedVolume + (usableBalance * lastCloseMinusSlippage), 2); - totalFeeCosts = Number(BigNumber(totalFeeCosts).plus(BigNumber(BTCBalance).times(BigNumber(transactionFee).dividedBy(BigNumber(100))).times(lastCloseMinusSlippage).round(2))); + totalFeeCosts = tools.round(totalFeeCosts + (BTCBalance * (transactionFee / 100) * lastCloseMinusSlippage), 2); - USDBalance = Number(BigNumber(USDBalance).plus(BigNumber(usableBalance).times(BigNumber(lastCloseMinusSlippage)).round(2))); - BTCBalance = 0; + USDBalance = tools.round(USDBalance + (usableBalance * lastCloseMinusSlippage), 2); + BTCBalance = 0; - if(USDBalance > highestUSDValue) { - highestUSDValue = USDBalance; - } else if(USDBalance < lowestUSDValue) { - lowestUSDValue = USDBalance; - } + if(USDBalance > highestUSDValue) { + highestUSDValue = USDBalance; + } else if(USDBalance < lowestUSDValue) { + lowestUSDValue = USDBalance; + } + + exitUSD = USDBalance; + + var tradeResult = tools.round(exitUSD - entryUSD, 2); + + if(exitUSD > entryUSD) { + winners += 1; + totalGain = tools.round(totalGain + tradeResult, 2); + if(tradeResult > bigWinner) {bigWinner = tradeResult;} + } else { + losers += 1; + totalLoss = tools.round(totalLoss + tradeResult, 2); + if(tradeResult < bigLoser) {bigLoser = tradeResult;} + } + + transactions += 1; + + if(stopLoss) { + slTransactions += 1; + logger.log('Stop loss Triggered an order:'); + } + + logger.log(new Date(latestCandlePeriod * 1000) + ' Placed sell order ' + usableBalance + ' @ ' + lastCloseMinusSlippage); + + pricemon.setPosition('sold', lastCloseMinusSlippage); + advisor.setPosition({pos: 'sold', price: lastCloseMinusSlippage}); + + } else { + + logger.debug('Wanted to place a ' + type + ' order @ ' + lastClose + ', but there are no more funds available to ' + type); + + } + +}; + +var calculate = function(err, result) { + + transactionFee = result.balance.fee; + + var loopArray = result.dbCandleSticks; + var csArray = result.aggregatedCandleSticks; + + if(loopArray.length > 0) { + + initialBalanceBTC = tools.round(USDBalance / _.first(loopArray).close, 2); - exitUSD = USDBalance; + var candleStickSizeSeconds = config.candleStickSizeMinutes * 60; - var tradeResult = Number(BigNumber(exitUSD).minus(BigNumber(entryUSD)).round(2)); + if(csArray.length > 0) { - if(exitUSD > entryUSD) { - winners += 1; - totalGain = Number(BigNumber(totalGain).plus(BigNumber(tradeResult)).round(2)); - if(tradeResult > bigWinner) {bigWinner = tradeResult;} - } else { - losers += 1; - totalLoss = Number(BigNumber(totalLoss).plus(BigNumber(tradeResult)).round(2)); - if(tradeResult < bigLoser) {bigLoser = tradeResult;} + csPeriod = _.first(csArray).period + candleStickSizeSeconds; + + } + + _.each(loopArray, function(cs) { + + lastClose = cs.close; + + if(stopLossEnabled) { + pricemon.check(cs.close); } - transactions += 1; + if(cs.period + 60 === csPeriod) { + + var candle = csArray.shift(); + latestCandlePeriod = candle.period; + if(csArray.length > 0) { + csPeriod = _.first(csArray).period + candleStickSizeSeconds; + } else { + csPeriod = 0; + } + + if(stopLossEnabled) { + + pricemon.update(candle, function(err) { + + logger.debug('Backtest: Created a new ' + config.candleStickSizeMinutes + ' minute candlestick!'); + logger.debug(JSON.stringify(candle)); + advisor.update(candle); + + }); + + } else { + + logger.debug('Backtest: Created a new ' + config.candleStickSizeMinutes + ' minute candlestick!'); + logger.debug(JSON.stringify(candle)); + advisor.update(candle); + + } - if(stopLoss) { - slTransactions += 1; - logger.log('Stop loss Triggered an order:'); } - logger.log('Placed sell order ' + usableBalance + ' @ ' + lastCloseMinusSlippage); + }); - pricemon.setPosition('sold', lastCloseMinusSlippage); - advisor.setPosition({pos: 'sold', price: lastCloseMinusSlippage}); + report(_.first(loopArray), _.last(loopArray)); } else { - logger.debug('Wanted to place a ' + type + ' order @ ' + lastClose + ', but there are no more funds available to ' + type); + logger.log('No data available to run backtester on.'); } }; -processor.on('initialized', function(){ - - exchangeapi.getBalance(true, function(err, result){ - - transactionFee = result.fee; - - var loopArray = storage.getAllCandlesSince(); - var csArray = storage.getFinishedAggregatedCandleSticks(config.candleStickSizeMinutes); - - intialBalanceBTC = Number(BigNumber(USDBalance).dividedBy(BigNumber(_.first(loopArray).close)).round(2)); - - var candleStickSizeSeconds = config.candleStickSizeMinutes * 60; - - if(csArray.length > 0) { - - csPeriod = _.first(csArray).period + candleStickSizeSeconds; - - } - - _.each(loopArray, function(cs) { - - lastClose = cs.close; - - if(stopLossEnabled) { - pricemon.check(cs.close); - } - - if(cs.period + 60 === csPeriod) { - var candle = csArray.shift(); - if(stopLossEnabled) { - pricemon.update(candle); - } - logger.debug('Backtest: Created a new ' + config.candleStickSizeMinutes + ' minute candlestick!'); - logger.debug(JSON.stringify(candle)); - advisor.update(candle); - if(csArray.length > 0) { - csPeriod = _.first(csArray).period + candleStickSizeSeconds; - } else { - csPeriod = 0; - } - } - - }); - - totalBalanceInUSD = Number(BigNumber(USDBalance).plus(BigNumber(BTCBalance).times(BigNumber(lastClose))).round(2)); - totalBalanceInBTC = Number(BigNumber(BTCBalance).plus(BigNumber(USDBalance).dividedBy(BigNumber(lastClose))).round(2)); - profit = Number(BigNumber(totalBalanceInUSD).minus(BigNumber(initialBalance)).round(2)); - profitPercentage = Number(BigNumber(profit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); - totalFeeCostsPercentage = Number(BigNumber(totalFeeCosts).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); - bhProfit = Number(BigNumber(_.last(loopArray).close).minus(_.first(loopArray).open).times(BigNumber(intialBalanceBTC)).round(2)); - bhProfitPercentage = Number(BigNumber(bhProfit).dividedBy(BigNumber(initialBalance)).times(BigNumber(100)).round(2)); - - startDate = moment(new Date(_.first(loopArray).period*1000)).format('DD-MM-YYYY HH:mm:ss'); - endDate = moment(new Date(_.last(loopArray).period*1000)).format('DD-MM-YYYY HH:mm:ss'); - - averageGain = Number(BigNumber(totalGain).dividedBy(winners).round(2)); - averageLoss = Number(BigNumber(totalLoss).dividedBy(losers).round(2)); - - logger.log('----------Report----------'); - logger.log('Transaction Fee: ' + transactionFee + '%'); - logger.log('Initial Balance: ' + initialBalance); - logger.log('Initial Balance BTC: ' + intialBalanceBTC); - logger.log('Final Balance: ' + totalBalanceInUSD); - logger.log('Final Balance BTC: ' + totalBalanceInBTC); - logger.log('Winning trades : ' + winners + ' Losing trades: ' + losers); - logger.log('Biggest winner: ' + bigWinner + ' Biggest loser: ' + bigLoser); - logger.log('Average winner: ' + averageGain + ' Average loser: ' + averageLoss); - logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); - logger.log('Buy and Hold Profit: ' + bhProfit + ' (' + bhProfitPercentage + '%)'); - logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); - logger.log('Total traded volue: ' + totalTradedVolume); - logger.log('Highest - Lowest USD Balance: ' + highestUSDValue + ' - ' + lowestUSDValue); - logger.log('Open Price: ' + _.first(loopArray).open); - logger.log('Close Price: ' + _.last(loopArray).close); - logger.log('Start - End Date: ' + startDate + ' - ' + endDate); - logger.log('Transactions: ' + transactions); - logger.log('Stop Loss Transactions: ' + slTransactions); - logger.log('--------------------------'); +var report = function(firstCs, lastCs) { - }); + totalBalanceInUSD = tools.round(USDBalance + (BTCBalance * lastClose), 2); + totalBalanceInBTC = tools.round(BTCBalance + (USDBalance / lastClose), 2); + profit = tools.round(totalBalanceInUSD - initialBalance, 2); + profitPercentage = tools.round(profit / initialBalance * 100, 2); + totalFeeCostsPercentage = tools.round(totalFeeCosts / initialBalance * 100, 2); + bhProfit = tools.round((lastCs.close - firstCs.open) * initialBalanceBTC, 2); + bhProfitPercentage = tools.round(bhProfit / initialBalance * 100, 2); -}); + if(totalBalanceInUSD > highestUSDValue) { + highestUSDValue = totalBalanceInUSD; + } + + startDate = moment(new Date(firstCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); + endDate = moment(new Date(lastCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); + + averageGain = tools.round(totalGain / winners, 2); + averageLoss = tools.round(totalLoss / losers, 2); + + logger.log('----------Report----------'); + logger.log('Transaction Fee: ' + transactionFee + '%'); + logger.log('Initial Balance: ' + initialBalance); + logger.log('Initial Balance BTC: ' + initialBalanceBTC); + logger.log('Final Balance: ' + totalBalanceInUSD); + logger.log('Final Balance BTC: ' + totalBalanceInBTC); + logger.log('Winning trades : ' + winners + ' Losing trades: ' + losers); + logger.log('Biggest winner: ' + bigWinner + ' Biggest loser: ' + bigLoser); + logger.log('Average winner: ' + averageGain + ' Average loser: ' + averageLoss); + logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); + logger.log('Buy and Hold Profit: ' + bhProfit + ' (' + bhProfitPercentage + '%)'); + logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); + logger.log('Total traded volue: ' + totalTradedVolume); + logger.log('Highest - Lowest USD Balance: ' + highestUSDValue + ' - ' + lowestUSDValue); + logger.log('Open Price: ' + firstCs.open); + logger.log('Close Price: ' + lastCs.close); + logger.log('Start - End Date: ' + startDate + ' - ' + endDate); + logger.log('Transactions: ' + transactions); + logger.log('Stop Loss Transactions: ' + slTransactions); + logger.log('--------------------------'); + +}; + +var start = function() { + async.series( + { + balance: function(cb) {exchangeapi.getBalance(true, cb);}, + dbCandleSticks: function(cb) {storage.getAllCandlesSince(0, cb);}, + aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(config.candleStickSizeMinutes, cb);} + }, + calculate + ); +}; advisor.on('advice', function(advice){ @@ -262,4 +302,4 @@ pricemon.on('advice', function(advice) { }); -processor.initialize(); +start(); diff --git a/config.sample.js b/config.sample.js index b72af3a..173c9d2 100644 --- a/config.sample.js +++ b/config.sample.js @@ -54,7 +54,7 @@ config.orderKeepAliveMinutes = config.candleStickSizeMinutes / 10; config.indicatorSettings = { indicator: 'MACD', // Choices: Any indicator from the indicators folder - options: {neededPeriods: 26, longPeriods: 26, shortPeriods: 12, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0} + options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0} // Options needed for your indicator (Look them up in the indicator's file) }; //------------------------------IndicatorSettings diff --git a/indicators/MACD.js b/indicators/MACD.js index 9ee8d1f..8845a1f 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -1,5 +1,5 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var indicator = function(options) { @@ -24,10 +24,10 @@ var calculateEma = function(periods, priceToday, previousEma) { previousEma = priceToday; } - var k = BigNumber(2).dividedBy(BigNumber(periods+1)); - var ema = (BigNumber(priceToday).times(k)).plus(BigNumber(previousEma).times(BigNumber(1).minus(k))); + var k = 2 / (periods + 1); + var ema = (priceToday * k) + (previousEma * (1 - k)); - return BigNumber(ema).round(8); + return ema; }; //-------------------------------------------------------------------------------HelperFunctions @@ -39,12 +39,12 @@ indicator.prototype.calculate = function(cs) { var usePrice = cs.close; - var emaLong = Number(calculateEma(this.options.longPeriods, usePrice, this.previousIndicator.emaLong)); - var emaShort = Number(calculateEma(this.options.shortPeriods, usePrice, this.previousIndicator.emaShort)); + var emaLong = calculateEma(this.options.longPeriods, usePrice, this.previousIndicator.emaLong); + var emaShort = calculateEma(this.options.shortPeriods, usePrice, this.previousIndicator.emaShort); - var macd = Number(BigNumber(emaShort).minus(BigNumber(emaLong))); - var macdSignal = Number(calculateEma(this.options.emaPeriods, macd, this.previousIndicator.macdSignal)); - var macdHistogram = Number(BigNumber(macd).minus(BigNumber(macdSignal)).round(2)); + var macd = emaShort - emaLong; + var macdSignal = calculateEma(this.options.emaPeriods, macd, this.previousIndicator.macdSignal); + var macdHistogram = tools.round(macd - macdSignal, 2); this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'macd': macd, 'macdSignal': macdSignal, 'result': macdHistogram}; diff --git a/indicators/PPO.js b/indicators/PPO.js index b9b91bf..a980573 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -1,5 +1,5 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var indicator = function(options) { @@ -24,10 +24,10 @@ var calculateEma = function(periods, priceToday, previousEma) { previousEma = priceToday; } - var k = BigNumber(2).dividedBy(BigNumber(periods+1)); - var ema = (BigNumber(priceToday).times(k)).plus(BigNumber(previousEma).times(BigNumber(1).minus(k))); + var k = 2 / (periods + 1); + var ema = (priceToday * k) + (previousEma * (1 - k)); - return BigNumber(ema).round(8); + return ema; }; //-------------------------------------------------------------------------------HelperFunctions @@ -39,12 +39,12 @@ indicator.prototype.calculate = function(cs) { var usePrice = cs.close; - var emaLong = Number(calculateEma(this.options.longPeriods, usePrice, this.previousIndicator.emaLong)); - var emaShort = Number(calculateEma(this.options.shortPeriods, usePrice, this.previousIndicator.emaShort)); + var emaLong = calculateEma(this.options.longPeriods, usePrice, this.previousIndicator.emaLong); + var emaShort = calculateEma(this.options.shortPeriods, usePrice, this.previousIndicator.emaShort); - var PPO = Number(BigNumber(emaShort).minus(BigNumber(emaLong)).dividedBy(BigNumber(emaLong)).times(BigNumber(100)).round(8)); - var PPOSignal = Number(calculateEma(this.options.emaPeriods, PPO, this.previousIndicator.PPOSignal)); - var PPOHistogram = Number(BigNumber(PPO).minus(BigNumber(PPOSignal)).round(2)); + var PPO = ((emaShort - emaLong) / emaLong) * 100; + var PPOSignal = calculateEma(this.options.emaPeriods, PPO, this.previousIndicator.PPOSignal); + var PPOHistogram = tools.round(PPO - PPOSignal, 2); this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'PPO': PPO, 'PPOSignal': PPOSignal, 'result': PPOHistogram}; diff --git a/indicators/PSAR.js b/indicators/PSAR.js index 35136e1..9e2afa5 100644 --- a/indicators/PSAR.js +++ b/indicators/PSAR.js @@ -1,5 +1,5 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var indicator = function(options) { @@ -23,10 +23,10 @@ var calculatePSAR = function(previousPSAR, previousEP, previousAF, previousTrend var temp; if(previousTrend === 1) { - temp = Number(BigNumber(previousPSAR).plus(BigNumber(previousAF).times(BigNumber(previousEP).minus(BigNumber(previousPSAR)))).round(2)); + temp = tools.round(previousPSAR + (previousAF * (previousEP - previousPSAR)), 2); PSAR = _.min([temp, limit]); } else if (previousTrend === -1) { - temp = Number(BigNumber(previousPSAR).minus(BigNumber(previousAF).times(BigNumber(previousPSAR).minus(BigNumber(previousEP)))).round(2)); + temp = tools.round(previousPSAR - (previousAF * (previousPSAR - previousEP)), 2); PSAR = _.max([temp,limit]); } @@ -93,7 +93,7 @@ var calculateAF = function(EP, previousEP, trend, previousTrend, previousAF, AFI if(EP !== previousEP) {EPChanged = true;} if(EPChanged && trend === previousTrend && previousAF < maximumAF) { - AF = Number(BigNumber(previousAF).plus(BigNumber(AFIncrement)).round(2)); + AF = tools.round(previousAF + AFIncrement, 2); } else if (trend !== previousTrend) { AF = AFIncrement; } else { @@ -116,8 +116,8 @@ indicator.prototype.calculate = function(cs) { } if(!this.firstCandleDone) { - this.previousPSAR = Number(BigNumber(cs.low).round(2)); - this.previousEP = Number(BigNumber(cs.high).round(2)); + this.previousPSAR = tools.round(cs.low, 2); + this.previousEP = tools.round(cs.high, 2); this.previousAF = this.options.AFIncrement; this.previousTrend = 1; this.firstCandleDone = true; diff --git a/indicators/template.js b/indicators/template.js index eec76cd..aadc6cc 100644 --- a/indicators/template.js +++ b/indicators/template.js @@ -4,7 +4,7 @@ process.exit(); //-------------------- REMOVE THIS BLOCK var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var indicator = function(options) { diff --git a/package.json b/package.json index 275cc19..f44f7f0 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { "name": "BitBot", - "version": "0.8.5", + "version": "0.9.0", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { "async": "0.9.0", - "bignumber.js": "1.2.1", - "bitstamp-api": "0.1.8", + "flat": "1.3.0", + "bitstamp-api": "0.1.9", "mime": "1.2.11", "moment": "2.4.0", - "mongojs": "0.14.2", + "mongojs": "0.15.1", "pushover-notifications": "0.1.5", "underscore": "1.6.0", "ws": "0.4.31", - "kraken-api": "0.1.5", + "kraken-exchange-api": "0.1.0", "winston": "0.8.0" }, "scripts": { diff --git a/services/candleaggregator.js b/services/candleaggregator.js index 4455ac8..d7d3424 100644 --- a/services/candleaggregator.js +++ b/services/candleaggregator.js @@ -1,11 +1,12 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var aggregator = function(candleStickSizeMinutes, storage, logger) { this.storage = storage; this.candleStickSize = candleStickSizeMinutes; this.logger = logger; + this.previousCompleteCandleStickPeriod = 0; _.bindAll(this, 'update'); @@ -19,30 +20,34 @@ Util.inherits(aggregator, EventEmitter); aggregator.prototype.update = function() { - if(this.storage.length(this.candleStickSize) > 0) { + this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSize, function(err, completeCandleStick) { - this.previousCandlePeriod = this.storage.getLastNonEmptyPeriod(this.candleStickSize); + if(completeCandleStick) { - var cs = this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSize); + if(this.previousCompleteCandleStickPeriod === 0) { - this.latestCandlePeriod = this.storage.getLastNonEmptyPeriod(this.candleStickSize); + this.previousCompleteCandleStickPeriod = completeCandleStick.period; - if(this.latestCandlePeriod > this.previousCandlePeriod) { + } - this.logger.log('Created a new ' + this.candleStickSize + ' minute candlestick!'); - this.logger.log(JSON.stringify(cs)); + if(completeCandleStick.period !== this.previousCompleteCandleStickPeriod) { - this.emit('update', cs); + this.logger.log('Created a new ' + this.candleStickSize + ' minute candlestick!'); + this.logger.log(JSON.stringify(completeCandleStick)); - this.storage.removeOldCandles(); + this.previousCompleteCandleStickPeriod = completeCandleStick.period; - } + this.storage.removeOldDBCandles(this.candleStickSize, function(err) { + + this.emit('update', completeCandleStick); - } else { + }.bind(this)); - this.storage.getFinishedAggregatedCandleSticks(this.candleStickSize); + } + + } - } + }.bind(this)); }; diff --git a/services/candlestorage.js b/services/candlestorage.js deleted file mode 100644 index b9a50be..0000000 --- a/services/candlestorage.js +++ /dev/null @@ -1,372 +0,0 @@ -var _ = require('underscore'); -var BigNumber = require('bignumber.js'); -var tools = require('../util/tools.js'); - -var storage = function(db, logger) { - - this.candleSticksCollection = []; - this.db = db; - this.logger = logger; - - _.bindAll(this, 'selectCollection', 'set', 'push', 'removeOldCandles', 'flush', 'getAllCandlesSince', 'getLastNCandles', 'getLastPeriod', 'getLastNonEmptyPeriod', 'getLastClose', 'getLastNonEmptyClose', 'getCandle', 'length', 'getAverageCandleStickSize', 'generateWebServerArray', 'getFinishedAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getAggregatedCandleSticks', 'materialise', 'getDBCandles'); - -}; - -storage.prototype.selectCollection = function(candleStickSize) { - - var selectedSize = candleStickSize; - - if(!candleStickSize) { - selectedSize = 1; - } - - var candleStickArray = _.find(this.candleSticksCollection, function(entry) { - return entry.type === selectedSize; - }); - - if(!candleStickArray) { - - this.candleSticksCollection.push({'type': selectedSize, 'candleSticks': []}); - candleStickArray = _.find(this.candleSticksCollection, function(entry) { - return entry.type === selectedSize; - }); - } - - return candleStickArray; - -}; - -storage.prototype.set = function(candleSticks, candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - candleStickArray.candleSticks = []; - - candleSticks.forEach(function(candleStick){ - candleStickArray.candleSticks.push(candleStick); - }); - -}; - -storage.prototype.push = function(candleStick, candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var updated = false; - - candleStickArray.candleSticks = _.map(candleStickArray.candleSticks, function(entry) { - - if(entry.period === candleStick.period){ - updated = true; - return candleStick; - } else { - return entry; - } - - }, this); - - if(!updated){ - - candleStickArray.candleSticks.push(candleStick); - - } - -}; - -storage.prototype.removeOldCandles = function() { - - var maxCandleSize = _.max(this.candleSticksCollection, function(collection) { - return collection.type; - }).type; - - var maxCandleSizeSeconds = maxCandleSize * 60; - - _.each(this.candleSticksCollection, function(collection) { - - var candleStickSize = collection.type; - - var candleStickSizeSeconds = candleStickSize * 60; - - var now = Math.floor(tools.unixTimeStamp(new Date().getTime()) / candleStickSizeSeconds) * candleStickSizeSeconds; - var oldPeriod = now - (maxCandleSizeSeconds * 2000); - - var candleStickArray = this.selectCollection(candleStickSize); - - candleStickArray.candleSticks = _.filter(candleStickArray.candleSticks, function(candleStick){ - return candleStick.period > oldPeriod; - }); - - if(candleStickSize === 1) { - this.db.removeOldDBCandles(oldPeriod); - } - - }, this); - -}; - -storage.prototype.flush = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - candleStickArray.candleSticks = []; - -}; - -storage.prototype.getAllCandlesSince = function(period, candleStickSize) { - - var filterPeriod = period; - - if(!period) { - filterPeriod = 0; - } - - var candleStickArray = this.selectCollection(candleStickSize); - - var array = _.filter(candleStickArray.candleSticks, function(candleStick) { - - return candleStick.period >= filterPeriod; - - }); - - return array; - -}; - -storage.prototype.getLastNCandles = function(amount, candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var N = amount; - - if(!amount) { - - N = 1; - - } - - return _.last(candleStickArray.candleSticks,N); - -}; - -storage.prototype.getLastPeriod = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var dsLength = candleStickArray.candleSticks.length; - - if(dsLength === 0) { - return 0; - } else { - return _.last(candleStickArray.candleSticks).period; - } - -}; - -storage.prototype.getLastNonEmptyPeriod = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var dsLength = candleStickArray.candleSticks.length; - - if(dsLength === 0) { - return 0; - } else { - var array = _.filter(candleStickArray.candleSticks, function(candleStick) { - return candleStick.volume > 0; - }); - if(array.length === 0) { - return 0; - } else { - return _.last(array).period; - } - } - -}; - -storage.prototype.getLastClose = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var dsLength = candleStickArray.candleSticks.length; - - if(dsLength === 0) { - return 0; - } else { - return _.last(candleStickArray.candleSticks).close; - } - -}; - -storage.prototype.getLastNonEmptyClose = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var dsLength = candleStickArray.candleSticks.length; - - if(dsLength === 0) { - return; - } else { - var array = _.filter(candleStickArray.candleSticks, function(candleStick) { - return candleStick.volume > 0; - }); - if(array.length === 0) { - return 0; - } else { - return _.last(array).close; - } - } - -}; - -storage.prototype.getCandle = function(period, candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var result = _.find(candleStickArray.candleSticks, function(candleStick) { - return candleStick.period === period; - }); - - if(!result) { - return {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - } else { - return result; - } - -}; - -storage.prototype.length = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - return candleStickArray.candleSticks.length; - -}; - -storage.prototype.getAverageCandleStickSize = function(csamount, candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var array = _.first(_.last(candleStickArray.candleSticks, csamount + 1), csamount); - - var average = 0; - - if(array.length > 0) { - average = Number(BigNumber(_.reduce(array, function(memo, entry){ return Number(BigNumber(memo).plus(Math.abs(Number(BigNumber(entry.close).minus(entry.open))))); }, 0)).dividedBy(BigNumber(csamount)).round(2)); - } - - return average; - -}; - -storage.prototype.generateWebServerArray = function(period, candleStickSize) { - - var array = this.getAllCandlesSince(period, candleStickSize); - - var result = []; - - _.each(array,function(entry){ - - result.push([entry.period * 1000, entry.open, entry.high, entry.low, entry.close, entry.volume]); - - }); - - return result; - -}; - -storage.prototype.getFinishedAggregatedCandleSticks = function(candleStickSize) { - - var array = this.getAggregatedCandleSticks(candleStickSize); - array = _.filter(array, function(entry){ return entry.period !== _.last(array).period; }); - - return array; - -}; - -storage.prototype.getLastCompleteAggregatedCandleStick = function(candleStickSize) { - - var array = this.getAggregatedCandleSticks(candleStickSize); - array = _.filter(array, function(entry){ return entry.period !== _.last(array).period; }); - - return _.last(array); - -}; - -storage.prototype.getAggregatedCandleSticks = function(candleStickSize) { - - var candleStickArray = this.selectCollection(candleStickSize); - - var candleStickSizeSeconds = 60 * candleStickSize; - - var latestAggregatedPeriod = this.getLastNonEmptyPeriod(candleStickSize) - candleStickSizeSeconds; - - var candleSticks = this.getAllCandlesSince(latestAggregatedPeriod, 1); - - var startTimeStamp = (Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds) + candleStickSizeSeconds; - var stopTimeStamp = _.last(candleSticks).period; - - for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { - - var beginPeriod = i; - var endPeriod = beginPeriod + candleStickSizeSeconds; - - var currentCandleStick = {'period':beginPeriod,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - - var relevantSticks = _.filter(candleSticks, function(candleStick) { - - return candleStick.period >= beginPeriod && candleStick.period < endPeriod; - - },this); - - var vrelevantSticks = _.filter(relevantSticks, function(candleStick) { - - return candleStick.volume > 0; - - },this); - - if(vrelevantSticks.length > 0) { - relevantSticks = vrelevantSticks; - } - - currentCandleStick.open = relevantSticks[0].open; - currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; - currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; - currentCandleStick.close = relevantSticks[relevantSticks.length - 1].close; - currentCandleStick.volume = _.reduce(relevantSticks, function(memo, entry) { return Number(BigNumber(memo).plus(BigNumber(entry.volume)).round(8)); }, 0); - if(currentCandleStick.volume === 0) { - currentCandleStick.vwap = currentCandleStick.close; - } else { - currentCandleStick.vwap = Number(BigNumber(_.reduce(relevantSticks, function(memo, entry) { - - return Number(BigNumber(memo).plus(BigNumber(entry.vwap).times(BigNumber(entry.volume))).round(2)); - - }, 0)).dividedBy(currentCandleStick.volume).round(2)); - } - - this.push(currentCandleStick, candleStickSize); - - } - - return candleStickArray.candleSticks; - -}; - -storage.prototype.materialise = function(callback) { - var candleStickArray = this.selectCollection(1); - this.db.materialise(candleStickArray.candleSticks, callback); -}; - -storage.prototype.getDBCandles = function(callback) { - this.db.getDBCandles(function(err, storageCandleSticks) { - - if(!err && storageCandleSticks) { - this.set(storageCandleSticks); - callback(null); - } else { - callback(err); - } - - }.bind(this)); -}; - -module.exports = storage; diff --git a/services/dataprocessor.js b/services/dataprocessor.js index 0c0d374..2fab125 100644 --- a/services/dataprocessor.js +++ b/services/dataprocessor.js @@ -1,5 +1,4 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); var async = require('async'); var tools = require('../util/tools.js'); @@ -9,7 +8,7 @@ var processor = function(storage, logger) { this.storage = storage; this.logger = logger; - _.bindAll(this, 'updateCandleStick', 'createBaseCandleSticks', 'processInitialLoad', 'processUpdate', 'initialize', 'updateCandleDB'); + _.bindAll(this, 'updateCandleStick', 'createCandleSticks', 'processUpdate', 'updateCandleDB'); }; @@ -32,14 +31,14 @@ processor.prototype.updateCandleStick = function (candleStick, tick) { } else { - var currentVwap = BigNumber(candleStick.vwap).times(BigNumber(candleStick.volume)); - var newVwap = BigNumber(tick.price).times(BigNumber(tick.amount)); + var currentVwap = candleStick.vwap * candleStick.volume; + var newVwap = tick.price * tick.amount; candleStick.high = _.max([candleStick.high, tick.price]); candleStick.low = _.min([candleStick.low, tick.price]); - candleStick.volume = Number(BigNumber(candleStick.volume).plus(BigNumber(tick.amount)).round(8)); - candleStick.vwap = Number(currentVwap.plus(newVwap).dividedBy(BigNumber(candleStick.volume)).round(2)); + candleStick.volume = tools.round(candleStick.volume + tick.amount, 8); + candleStick.vwap = tools.round((currentVwap + newVwap) / candleStick.volume, 2); } @@ -49,139 +48,129 @@ processor.prototype.updateCandleStick = function (candleStick, tick) { }; -processor.prototype.createBaseCandleSticks = function (callback) { +processor.prototype.createCandleSticks = function(ticks, callback) { - var previousClose = 0; + if(ticks.length > 0) { - if(this.ticks.length > 0) { + this.storage.getLastNonEmptyPeriod(function(err, lastStoragePeriod) { - var candleStickSizeSeconds = 60; + this.storage.getLastNonEmptyClose(function(err, lastNonEmptyClose) { - var tickTimeStamp = this.ticks[0].date; + var candleStickSizeSeconds = 60; - var lastStoragePeriod = this.storage.getLastNonEmptyPeriod(); - var firstTickCandleStick = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); + var toBePushed = []; - if(lastStoragePeriod < firstTickCandleStick && lastStoragePeriod !== 0) { - tickTimeStamp = lastStoragePeriod + candleStickSizeSeconds; - } - - var now = tools.unixTimeStamp(new Date().getTime()); - - var startTimeStamp = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); - var stopTimeStamp = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); + var previousClose = lastNonEmptyClose; - var endTimeStamp = startTimeStamp + candleStickSizeSeconds; + var tickTimeStamp = ticks[0].date; - while(endTimeStamp < this.ticks[0].date) { + var firstTickCandleStick = (Math.floor(ticks[0].date/candleStickSizeSeconds)*candleStickSizeSeconds); - previousClose = this.storage.getLastNonEmptyClose(); - - this.storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + if(lastStoragePeriod < firstTickCandleStick && lastStoragePeriod !== 0) { + tickTimeStamp = lastStoragePeriod + candleStickSizeSeconds; + } - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + var now = tools.unixTimeStamp(new Date().getTime()); - } + var startTimeStamp = (Math.floor(tickTimeStamp/candleStickSizeSeconds)*candleStickSizeSeconds); + var stopTimeStamp = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); - var currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + var endTimeStamp = startTimeStamp + candleStickSizeSeconds; - this.ticks.forEach(function(tick){ + while(endTimeStamp < ticks[0].date) { - tickTimeStamp = tick.date; + toBePushed.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); - while(tickTimeStamp >= endTimeStamp + candleStickSizeSeconds) { + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - if(currentCandleStick.volume > 0) { - this.storage.push(currentCandleStick); } - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; - - previousClose = this.storage.getLastNonEmptyClose(); + var currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0,'vwap':undefined}; - this.storage.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + ticks.forEach(function(tick) { - } + tickTimeStamp = tick.date; - if(tickTimeStamp >= endTimeStamp) { + if(toBePushed.length > 0) { + previousClose = _.last(toBePushed).close; + } - if(currentCandleStick.volume > 0) { - this.storage.push(currentCandleStick); - } + while(tickTimeStamp >= endTimeStamp + candleStickSizeSeconds) { - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + if(currentCandleStick.volume > 0) { + toBePushed.push(currentCandleStick); + } - currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - } + toBePushed.push({'period':startTimeStamp,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); - if(tickTimeStamp >= startTimeStamp && tickTimeStamp < endTimeStamp) { + } - currentCandleStick = this.updateCandleStick(currentCandleStick,tick); + if(tickTimeStamp >= endTimeStamp) { - } + if(currentCandleStick.volume > 0) { + toBePushed.push(currentCandleStick); + } - }.bind(this)); + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - if(currentCandleStick.volume > 0) { + currentCandleStick = {'period':startTimeStamp,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - this.storage.push(currentCandleStick); + } - startTimeStamp = endTimeStamp; - endTimeStamp = endTimeStamp + candleStickSizeSeconds; + if(tickTimeStamp >= startTimeStamp && tickTimeStamp < endTimeStamp) { - } + currentCandleStick = this.updateCandleStick(currentCandleStick,tick); - for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { + } - var beginPeriod = i; - var endPeriod = beginPeriod + candleStickSizeSeconds; + }.bind(this)); - previousClose = this.storage.getLastNonEmptyClose(); + if(currentCandleStick.volume > 0) { - this.storage.push({'period':beginPeriod,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); + toBePushed.push(currentCandleStick); - } + startTimeStamp = endTimeStamp; + endTimeStamp = endTimeStamp + candleStickSizeSeconds; - callback(null); + } - } else { + if(toBePushed.length > 0) { + previousClose = _.last(toBePushed).close; + } - callback(null); + for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { - } + var beginPeriod = i; + var endPeriod = beginPeriod + candleStickSizeSeconds; -}; + toBePushed.push({'period':beginPeriod,'open':previousClose,'high':previousClose,'low':previousClose,'close':previousClose,'volume':0, 'vwap':previousClose}); -processor.prototype.processInitialLoad = function(err, result) { + } - if(err) { + async.eachSeries(toBePushed, function(candleStick, nextPush) { - var parsedError = JSON.stringify(err); + this.storage.push(candleStick, nextPush); - if(err.stack) { - parsedError = err.stack; - } + }.bind(this), callback); - this.logger.error('Couldn\'t create candlesticks due to a database error'); - this.logger.error(parsedError); + }.bind(this)); - process.exit(); + }.bind(this)); } else { - this.emit('initialized'); + callback(null); } }; -processor.prototype.processUpdate = function(err, result) { - - this.ticks = []; +processor.prototype.processUpdate = function(err) { if(err) { @@ -198,43 +187,40 @@ processor.prototype.processUpdate = function(err, result) { } else { - var latestCandleStick = this.storage.getLastNCandles(1)[0]; + this.storage.getLastNCandles(1, function(err, candleSticks) { - if(!this.initialDBWriteDone) { - this.emit('initialDBWrite'); - this.initialDBWriteDone = true; - } else { + var latestCandleStick = candleSticks[0]; - this.emit('update', latestCandleStick); + if(!this.initialDBWriteDone) { - } + this.emit('initialDBWrite'); + this.initialDBWriteDone = true; - } + } else { -}; + this.emit('update', latestCandleStick); -processor.prototype.initialize = function() { + } - async.waterfall([ - this.storage.getDBCandles - ], this.processInitialLoad); + }.bind(this)); + + } }; processor.prototype.updateCandleDB = function(ticks) { - var period = this.storage.getLastNonEmptyPeriod(); + this.storage.getLastNonEmptyPeriod(function(err, period) { + + var newTicks = _.filter(ticks,function(tick){ - this.ticks = _.filter(ticks,function(tick){ + return tick.date >= period; - return tick.date >= period; + }); - }); + this.createCandleSticks(newTicks, this.processUpdate); - async.waterfall([ - this.createBaseCandleSticks, - this.storage.materialise - ], this.processUpdate); + }.bind(this)); }; diff --git a/services/db.js b/services/db.js deleted file mode 100644 index 862984d..0000000 --- a/services/db.js +++ /dev/null @@ -1,188 +0,0 @@ -var _ = require('underscore'); -var mongo = require('mongojs'); -var async = require('async'); - -var db = function(exchangeSettings, mongoConnectionString, logger) { - - this.pair = exchangeSettings.currencyPair.pair; - this.exchange = exchangeSettings.exchange; - this.dbCollectionName = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; - this.mongoConnectionString = mongoConnectionString; - this.logger = logger; - - _.bindAll(this, 'materialise', 'removeOldDBCandles', 'getDBCandles', 'getInitialBalance', 'setInitialBalance'); - -}; - -db.prototype.materialise = function(candleStickArray, callback) { - - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); - - csCollection.find({volume: {$gt:0}}).sort({period:-1}).limit(1,function(err, sticks) { - - if(err) { - - callback(err); - - } else { - - var filterPeriod = 0; - - if(sticks.length > 0) { - - filterPeriod = sticks[0].period; - - } - - materialiseCs = _.filter(candleStickArray, function(cs){ - - return cs.period >= filterPeriod; - - }); - - if(materialiseCs.length > 0) { - - async.eachSeries(materialiseCs, function(cs, cb) { - - csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, doc) { - - if(err) { - - cb(err); - - } else { - - cb(); - - } - - }); - - }, function(err) { - - csDatastore.close(); - - if(err) { - - callback(err); - - } else { - - callback(null); - - } - - }); - - } else { - - callback(null); - - } - - } - - }); - -}; - -db.prototype.removeOldDBCandles = function(filterPeriod) { - - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); - - csCollection.remove({ period: { $lte: filterPeriod } }, function(err, resp) { - - csDatastore.close(); - - }); - -}; - -db.prototype.getDBCandles = function(callback) { - - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); - - csCollection.ensureIndex({period: 1}); - - csCollection.find({}).sort({period:1}, function(err, candleSticks) { - - csDatastore.close(); - - if(err) { - - callback(err); - - } else if(candleSticks.length > 0 ){ - - var storageCandleSticks = _.map(candleSticks, function(candleStick){ - return {'period':candleStick.period, 'open':candleStick.open, 'high':candleStick.high, 'low':candleStick.low, 'close':candleStick.close, 'volume':candleStick.volume, 'vwap':candleStick.vwap}; - }); - - callback(null, storageCandleSticks); - - } else { - - callback(null); - - } - - }.bind(this)); - -}; - -db.prototype.getInitialBalance = function(callback) { - - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection('balance'); - - csCollection.find({exchangePair: this.dbCollectionName}).limit(1, function(err, balance) { - - csDatastore.close(); - - if(err) { - - callback(err); - - } else if(balance.length > 0 ){ - - var initialBalance = balance[0].initialBalance; - - callback(null, initialBalance); - - } else { - - callback(null, null); - - } - - }.bind(this)); - -}; - -db.prototype.setInitialBalance = function(initialBalance, callback) { - - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection('balance'); - - csCollection.update({exchangePair: this.dbCollectionName}, {exchangePair: this.dbCollectionName, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { - - csDatastore.close(); - - if(err) { - - callback(err); - - } else { - - callback(null); - - } - - }.bind(this)); - -}; - -module.exports = db; diff --git a/services/loggingservice.js b/services/loggingservice.js index ed2a3ad..d12d4a7 100644 --- a/services/loggingservice.js +++ b/services/loggingservice.js @@ -3,7 +3,7 @@ var _ = require('underscore'); var winston = require('winston'); var fs = require('fs'); -var logger = function(debug) { +var logger = function(app, debug) { this.debugEnabled = debug; @@ -33,7 +33,7 @@ var logger = function(debug) { new (winston.transports.DailyRotateFile)({ 'timestamp': now, datePattern: '_dd-MM-yyyy.log', - filename: 'logs/debug', + filename: 'logs/' + app, level: 'DEBUG'}) ] }); diff --git a/services/ordermonitor.js b/services/ordermonitor.js index 16606c3..b0eb965 100644 --- a/services/ordermonitor.js +++ b/services/ordermonitor.js @@ -1,5 +1,4 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); var monitor = function(exchangeapi, logger) { diff --git a/services/pricemonitor.js b/services/pricemonitor.js index 7a593c8..0737bec 100644 --- a/services/pricemonitor.js +++ b/services/pricemonitor.js @@ -1,5 +1,5 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var monitor = function(slPercentageB, slPercentageS, candleStickSizeMinutes, storage, logger) { @@ -55,49 +55,60 @@ monitor.prototype.setPosition = function(pos, price) { this.position = 'bought'; this.posPrice = price; - this.checkPriceBought = Number(BigNumber(this.posPrice).times(BigNumber(1).minus(BigNumber(this.percentageBought).dividedBy(BigNumber(100))))); + this.checkPriceBought = this.posPrice * (1 - (this.percentageBought / 100)); } else if(pos === 'sold') { this.position = 'sold'; this.posPrice = price; - this.checkPriceSold = Number(BigNumber(this.posPrice).times(BigNumber(1).plus(BigNumber(this.percentageSold).dividedBy(BigNumber(100))))); + this.checkPriceSold = this.posPrice * (1 + (this.percentageSold / 100)); } }; -monitor.prototype.update = function(cs) { +monitor.prototype.update = function(cs, callback) { - var diff = cs.close - cs.open; - var size = Math.abs(Number(BigNumber(cs.close).minus(BigNumber(cs.open)).round(2))); - var averageSize = this.storage.getAverageCandleStickSize(10, this.candleStickSizeMinutes); + this.storage.getLastNCompleteAggregatedCandleSticks(10, this.candleStickSizeMinutes, function(err, completeCandleSticks) { - var change = Number(BigNumber(size).dividedBy(2).round(2)); + var averageSize = 0; - var newSl; + if(completeCandleSticks.length > 0) { + averageSize = tools.floor(_.reduce(completeCandleSticks, function(memo, entry){ return memo + Math.abs(entry.close - entry.open); }, 0) / 10, 2); + } - if(size >= averageSize * 2) { + var diff = cs.close - cs.open; + var size = Math.abs(tools.round(cs.close - cs.open, 2)); - if(this.position === 'bought' && diff > 0) { + var change = tools.round(size / 2, 2); - newSl = Number(BigNumber(this.checkPriceBought).plus(change)); + var newSl; - this.logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); + if(size >= averageSize * 2) { - this.checkPriceBought = newSl; + if(this.position === 'bought' && diff > 0) { - } else if(this.position === 'sold' && diff < 0) { + newSl = tools.round(this.checkPriceBought + change, 2); - newSl = Number(BigNumber(this.checkPriceSold).minus(change)); + this.logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); - this.logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); + this.checkPriceBought = newSl; - this.checkPriceSold = newSl; + } else if(this.position === 'sold' && diff < 0) { + + newSl = tools.round(this.checkPriceSold - change, 2); + + this.logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); + + this.checkPriceSold = newSl; + + } } - } + callback(null); + + }.bind(this)); }; diff --git a/services/profitreporter.js b/services/profitreporter.js index 8959f59..f5467de 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -1,11 +1,11 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var async = require('async'); -var reporter = function(currencyPair, db, exchangeapi, logger) { +var reporter = function(currencyPair, storage, exchangeapi, logger) { this.currencyPair = currencyPair; - this.db = db; + this.storage = storage; this.exchangeapi = exchangeapi; this.logger = logger; @@ -22,14 +22,14 @@ Util.inherits(reporter, EventEmitter); reporter.prototype.intialize = function(err, result) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); + this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 2); this.highestBid = _.first(result.orderBook.bids).currencyPrice; - this.assetBalanceInCurrency = BigNumber(this.assetBalance).times(BigNumber(this.highestBid)); + this.assetBalanceInCurrency = this.assetBalance * this.highestBid; - this.initalTotalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); + this.initalTotalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 2); - this.db.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { + this.storage.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { if(err) { @@ -64,14 +64,14 @@ reporter.prototype.createReport = function() { reporter.prototype.processBalance = function(err, result) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.assetBalance = Number(BigNumber(parseFloat(result.balance.assetAvailable)).round(2)); + this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 2); this.highestBid = _.first(result.orderBook.bids).currencyPrice; - this.assetBalanceInCurrency = BigNumber(this.assetBalance).times(BigNumber(this.highestBid)); + this.assetBalanceInCurrency = this.assetBalance * this.highestBid; - this.totalCurrencyBalance = Number(BigNumber(this.currencyBalance).plus(this.assetBalanceInCurrency).round(2)); - this.profitAbsolute = Number(BigNumber(this.totalCurrencyBalance).minus(this.initalTotalCurrencyBalance)); - this.profitPercentage = Number(BigNumber(this.profitAbsolute).dividedBy(BigNumber(this.initalTotalCurrencyBalance)).times(BigNumber(100)).round(2)); + this.totalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 2); + this.profitAbsolute = this.totalCurrencyBalance - this.initalTotalCurrencyBalance; + this.profitPercentage = tools.round((this.profitAbsolute / this.initalTotalCurrencyBalance) * 100, 2); if(this.includeReport) { this.createReport(); @@ -83,7 +83,7 @@ reporter.prototype.start = function(resetInitialBalances) { this.resetInitialBalances = resetInitialBalances; - this.db.getInitialBalance(function(err, result) { + this.storage.getInitialBalance(function(err, result) { if(err) { diff --git a/services/storage.js b/services/storage.js new file mode 100644 index 0000000..5e3adf5 --- /dev/null +++ b/services/storage.js @@ -0,0 +1,394 @@ +var _ = require('underscore'); +var mongo = require('mongojs'); +var async = require('async'); +var tools = require('../util/tools.js'); + +var storage = function(exchangeSettings, mongoConnectionString, logger) { + + this.pair = exchangeSettings.currencyPair.pair; + this.exchange = exchangeSettings.exchange; + this.dbCollectionName = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; + this.mongoConnectionString = mongoConnectionString; + this.logger = logger; + + _.bindAll(this, 'push', 'getLastNCandles', 'getAllCandles', 'getAllCandlesSince', 'getLastClose', 'getLastNonEmptyPeriod', 'getLastNonEmptyClose', 'getLastNCompleteAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getCompleteAggregatedCandleSticks', 'getLastNAggregatedCandleSticks', 'getAggregatedCandleSticks', 'calculateAggregatedCandleStick', 'aggregateCandleSticks', 'removeOldDBCandles', 'getInitialBalance', 'setInitialBalance'); + +}; + +storage.prototype.push = function(cs, callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, result) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else { + + callback(null); + + } + + }); + +}; + +storage.prototype.getLastNCandles = function(N, callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({}).sort({period:-1}).limit(N, function(err, candlesSticks) { + + csDatastore.close(); + + if(err) { + + callback(err, []); + + } else { + + callback(null, candlesSticks.reverse()); + + } + + }); + + +}; + +storage.prototype.getAllCandles = function(callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({}).sort({period:1}, function(err, candlesSticks) { + + csDatastore.close(); + + if(err) { + + callback(err, []); + + } else { + + callback(null, candlesSticks); + + } + + }); + + +}; + +storage.prototype.getAllCandlesSince = function(period, callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({period: { $gte: period }}).sort({period:1}, function(err, candlesSticks) { + + csDatastore.close(); + + if(err) { + + callback(err, []); + + } else { + + callback(null, candlesSticks); + + } + + }); + + +}; + +storage.prototype.getLastClose = function(callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({}).sort({period:-1}).limit(1, function(err, candleSticks) { + + csDatastore.close(); + + if(err) { + + callback(err, 0); + + } else { + + if(candleSticks.length > 0) { + callback(null, candleSticks[0].close); + } else { + callback(null, 0); + } + + } + + }); + +}; + +storage.prototype.getLastNonEmptyPeriod = function(callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { + + csDatastore.close(); + + if(err) { + + callback(err, 0); + + } else { + + if(candleSticks.length > 0) { + callback(null, candleSticks[0].period); + } else { + callback(null, 0); + } + + } + + }); + +}; + +storage.prototype.getLastNonEmptyClose = function(callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { + + csDatastore.close(); + + if(err) { + + callback(err, 0); + + } else { + + if(candleSticks.length > 0) { + callback(null, candleSticks[0].close); + } else { + callback(null, 0); + } + + } + + }); + +}; + +storage.prototype.getLastNCompleteAggregatedCandleSticks = function(N, candleStickSize, callback) { + + this.getLastNAggregatedCandleSticks(N + 1, candleStickSize, function(err, aggregatedCandleSticks) { + aggregatedCandleSticks.pop(); + callback(null, aggregatedCandleSticks); + }); + +}; + +storage.prototype.getLastCompleteAggregatedCandleStick = function(candleStickSize, callback) { + + this.getLastNAggregatedCandleSticks(2, candleStickSize, function(err, aggregatedCandleSticks) { + aggregatedCandleSticks.pop(); + callback(null, _.last(aggregatedCandleSticks)); + }); + +}; + +storage.prototype.getCompleteAggregatedCandleSticks = function(candleStickSize, callback) { + + this.getAggregatedCandleSticks(candleStickSize, function(err, aggregatedCandleSticks) { + aggregatedCandleSticks.pop(); + callback(null, aggregatedCandleSticks); + }); + +}; + +storage.prototype.getLastNAggregatedCandleSticks = function(N, candleStickSize, callback) { + + var candleStickSizeSeconds = candleStickSize * 60; + + var now = tools.unixTimeStamp(new Date().getTime()); + var closestCandleStick = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); + + var startRange = closestCandleStick - (candleStickSizeSeconds * N); + + this.getAllCandlesSince(startRange, function(err, candleSticks) { + + if(candleSticks.length > 0) { + + var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); + + callback(null, aggregatedCandleSticks); + + } else { + + callback(null, []); + + } + + }.bind(this)); + +}; + +storage.prototype.getAggregatedCandleSticks = function(candleStickSize, callback) { + + this.getAllCandlesSince(0, function(err, candleSticks) { + + if(candleSticks.length > 0) { + + var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); + + callback(null, aggregatedCandleSticks); + + } else { + + callback(null, []); + + } + + }.bind(this)); + +}; + +storage.prototype.calculateAggregatedCandleStick = function(period, relevantSticks) { + + var currentCandleStick = {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + + currentCandleStick.open = relevantSticks[0].open; + currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; + currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; + currentCandleStick.close = relevantSticks[relevantSticks.length - 1].close; + currentCandleStick.volume = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + entry.volume; }, 0), 8); + if(currentCandleStick.volume === 0) { + currentCandleStick.vwap = currentCandleStick.close; + } else { + currentCandleStick.vwap = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + (entry.vwap * entry.volume); }, 0) / currentCandleStick.volume, 2); + } + + return currentCandleStick; + +}; + +storage.prototype.aggregateCandleSticks = function(candleStickSize, candleSticks) { + + var candleStickSizeSeconds = 60 * candleStickSize; + + var aggregatedCandleSticks = []; + + var startTimeStamp = Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds; + var stopTimeStamp = _.last(candleSticks).period; + + var filterOnPeriod = function(candleStick) { return candleStick.period >= beginPeriod && candleStick.period < endPeriod; }; + var filterOnVolume = function(candleStick) { return candleStick.volume > 0; }; + + for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { + + var beginPeriod = i; + var endPeriod = beginPeriod + candleStickSizeSeconds; + + var relevantSticks = _.filter(candleSticks, filterOnPeriod); + + var vrelevantSticks = _.filter(relevantSticks, filterOnVolume); + + if(vrelevantSticks.length > 0) { + relevantSticks = vrelevantSticks; + } + + var currentCandleStick = this.calculateAggregatedCandleStick(beginPeriod, relevantSticks); + + aggregatedCandleSticks.push(currentCandleStick); + + } + + return aggregatedCandleSticks; + +}; + +storage.prototype.removeOldDBCandles = function(candleStickSize, callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.dbCollectionName); + + var candleStickSizeSeconds = candleStickSize * 60; + + var now = Math.floor(tools.unixTimeStamp(new Date().getTime()) / candleStickSizeSeconds) * candleStickSizeSeconds; + var oldPeriod = now - (candleStickSizeSeconds * 10000); + + csCollection.remove({ period: { $lt: oldPeriod } }, function(err, resp) { + + csDatastore.close(); + + callback(null); + + }); + +}; + +storage.prototype.getInitialBalance = function(callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); + + csCollection.find({exchangePair: this.dbCollectionName}).limit(1, function(err, balance) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else if(balance.length > 0 ){ + + var initialBalance = balance[0].initialBalance; + + callback(null, initialBalance); + + } else { + + callback(null, null); + + } + + }.bind(this)); + +}; + +storage.prototype.setInitialBalance = function(initialBalance, callback) { + + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); + + csCollection.update({exchangePair: this.dbCollectionName}, {exchangePair: this.dbCollectionName, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { + + csDatastore.close(); + + if(err) { + + callback(err); + + } else { + + callback(null); + + } + + }.bind(this)); + +}; + +module.exports = storage; diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index 396706f..d3a17e7 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -30,13 +30,15 @@ Util.inherits(advisor, EventEmitter); advisor.prototype.start = function() { - var candleSticks = this.storage.getFinishedAggregatedCandleSticks(this.candleStickSize); + this.storage.getLastNCompleteAggregatedCandleSticks(1000, this.candleStickSize, function(err, candleSticks) { - for(var i = 0; i < candleSticks.length; i++) { + for(var i = 0; i < candleSticks.length; i++) { - var result = this.selectedIndicator.calculate(candleSticks[i]); + var result = this.selectedIndicator.calculate(candleSticks[i]); - } + } + + }.bind(this)); }; diff --git a/services/tradingagent.js b/services/tradingagent.js index 59800d7..9aaba46 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -1,5 +1,5 @@ var _ = require('underscore'); -var BigNumber = require('bignumber.js'); +var tools = require('../util/tools.js'); var async = require('async'); var agent = function(tradingEnabled, exchangeSettings, storage, exchangeapi, logger) { @@ -45,7 +45,8 @@ agent.prototype.order = function(orderType) { async.series( { balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), - orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this) + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), + lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) }, process.bind(this) ); @@ -60,9 +61,9 @@ agent.prototype.calculateOrder = function(result) { var orderBook = result.orderBook; - var lastClose = this.storage.getLastClose(); - var minClose = Number(BigNumber(lastClose).times(BigNumber(0.9975)).round(2)); - var maxClose = Number(BigNumber(lastClose).times(BigNumber(1.0025)).round(2)); + var lastClose = result.lastClose; + var minClose = tools.round(lastClose * 0.9975, 2); + var maxClose = tools.round(lastClose * 1.0025, 2); this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.tradingFee +')'); @@ -79,13 +80,13 @@ agent.prototype.calculateOrder = function(result) { var lowestAsk = lastClose; - var lowestAskWithSlippage = Number(BigNumber(lowestAsk).times(BigNumber(1).plus(BigNumber(this.slippagePercentage).dividedBy(BigNumber(100)))).round(2)); - var balance = (BigNumber(this.orderDetails.currencyBalance).minus(BigNumber(this.tradingReserveCurrency))).times(BigNumber(1).minus(BigNumber(this.orderDetails.tradingFee).dividedBy(BigNumber(100)))); + var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 2); + var balance = (this.orderDetails.currencyBalance - this.tradingReserveCurrency) * (1 - (this.orderDetails.tradingFee / 100)); this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); this.orderDetails.price = lowestAskWithSlippage; - this.orderDetails.amount = Number(balance.dividedBy(BigNumber(this.orderDetails.price)).minus(BigNumber(0.005)).round(2)); + this.orderDetails.amount = tools.round((balance / this.orderDetails.price) - 0.005, 2); } else if(this.orderDetails.orderType === 'sell') { @@ -100,12 +101,12 @@ agent.prototype.calculateOrder = function(result) { var highestBid = lastClose; - var highestBidWithSlippage = Number(BigNumber(highestBid).times(BigNumber(1).minus(BigNumber(this.slippagePercentage).dividedBy(BigNumber(100)))).round(2)); + var highestBidWithSlippage = tools.round(highestBid * (1 - (this.slippagePercentage / 100)), 2); this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); this.orderDetails.price = highestBidWithSlippage; - this.orderDetails.amount = Number(BigNumber(this.orderDetails.assetBalance).minus(BigNumber(this.tradingReserveAsset))); + this.orderDetails.amount = tools.round(this.orderDetails.assetBalance - this.tradingReserveAsset, 2); } diff --git a/tests/cancelorder.js b/tests/cancelorder.js index 9e44000..db50b53 100644 --- a/tests/cancelorder.js +++ b/tests/cancelorder.js @@ -6,7 +6,7 @@ var exchangeapiservice = require('../services/exchangeapi.js'); var loggingservice = require('../services/loggingservice.js'); var _ = require('underscore'); -var logger = new loggingservice(config.debug); +var logger = new loggingservice('cancelOrderTest', config.debug); var api = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); api.getBalance(true, function(err, result) { diff --git a/tests/dbhealth.js b/tests/dbhealth.js index e85c198..ba85048 100644 --- a/tests/dbhealth.js +++ b/tests/dbhealth.js @@ -2,18 +2,14 @@ var _ = require('underscore'); var config = require('../config.js'); var loggingservice = require('../services/loggingservice.js'); -var database = require('../services/db.js'); -var candlestorage = require('../services/candlestorage.js'); +var storageservice = require('../services/storage.js'); var dataprocessor = require('../services/dataprocessor.js'); -var logger = new loggingservice(config.debug); -var db = new database(config.exchangeSettings, config.mongoConnectionString, logger); -var storage = new candlestorage(db, logger); +var logger = new loggingservice('dbHealthTest', config.debug); +var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var processor = new dataprocessor(storage, logger); -processor.on('initialized', function(){ - - var loopArray = storage.getAllCandlesSince(); +storage.getAllCandlesSince(0, function(err, loopArray) { var testPeriod = _.first(loopArray).period - 60; var success = true; @@ -41,5 +37,3 @@ processor.on('initialized', function(){ } }); - -processor.initialize(); diff --git a/util/tools.js b/util/tools.js index 8cedab7..fb12632 100644 --- a/util/tools.js +++ b/util/tools.js @@ -7,6 +7,22 @@ tools.prototype.unixTimeStamp = function(timestamp) { return Math.floor(timestamp/1000); }; +tools.prototype.getRandomInt = function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +tools.prototype.getRandomArbitrary = function(decimals, min, max) { + return (Math.random() * (max - min) + min).toFixed(decimals); +}; + +tools.prototype.round = function(value, decimals) { + return Number(Math.round(value+'e'+decimals)+'e-'+decimals); +}; + +tools.prototype.floor = function(value, decimals) { + return Number(Math.floor(value+'e'+decimals)+'e-'+decimals); +}; + var utiltools = new tools(); -module.exports = utiltools; +module.exports = utiltools; From 40d42cacabe182534ace883d19a0b3ce6597ef23 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 28 Oct 2014 22:24:41 +0100 Subject: [PATCH 32/57] v0.9.1 Backtester cosmetic changes - Backtester now displays configured Asset/Currency - Added some extra documentation on how the backtester works --- README.md | 28 +++++++++++++++------ app.js | 2 +- backtester.js | 65 +++++++++++++++++++++++++++++------------------- config.sample.js | 3 ++- package.json | 2 +- 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c47c587..b12a3cc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ BitBot is a Crypto-Currency trading bot and backtesting platform that connects to popular Bitcoin exchanges (Bitstamp, Kraken). It is written in javascript and runs on [Node.JS](http://nodejs.org). +BitBot is modular and supports multiple trading strategies / exchanges. + ## Dependencies - [Node.JS](http://nodejs.org) @@ -11,9 +13,7 @@ BitBot is a Crypto-Currency trading bot and backtesting platform that connects t Make sure you have the latest Node.JS version and MongoDB installed. -Clone this repository to a folder of your liking and execute the following command: - - npm install +Clone this repository to a folder of your liking and execute the following command: `npm install` Pay close attention to the log messages of NPM (there shouldn't be any errors). @@ -21,10 +21,7 @@ Pay close attention to the log messages of NPM (there shouldn't be any errors). - Overwrite your files with the latest versions from this repository - Compare you config.js file with the config.sample.js file to make sure there aren't any extra settings available -- Run the following command: - - - npm install +- Run the following command: `npm install` ## Configuration basics @@ -133,9 +130,24 @@ As of version 0.7 BitBot now uses indicators as small plugins. You can create yo For examples on how to use this template, go have a look at one of the existing indicators in the indicators folder. +## Backtester + +The provided backtester simulates the settings you provided in config.js over a set of data. That set of data is the data you collected by running app.js over a period of time. + +Before you run the backtester, make sure you configured the following items in your config.js file: + +- exchangeSettings +- apiSettings +- indicatorSettings +- backTesting + - initialAssetBalance + - initialCurrencyBalance + +The backtester needs access to your API settings to query your current trading fee. This will give you more accurate results when simulating. + ## Usage -Execute the following command in the folder where you installed BitBot: +To run BitBot, execute the following command in the folder where you installed BitBot: node app.js diff --git a/app.js b/app.js index e5bd079..911f9fd 100644 --- a/app.js +++ b/app.js @@ -170,7 +170,7 @@ var start = function() { //------------------------------AnnounceStart logger.log('------------------------------------------'); - logger.log('Starting BitBot v0.9.0'); + logger.log('Starting BitBot v0.9.1'); logger.log('Real Trading Enabled = ' + config.tradingEnabled); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index 07c784d..b54bf9c 100644 --- a/backtester.js +++ b/backtester.js @@ -24,13 +24,18 @@ var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config //------------------------------IntializeModules //------------------------------IntializeVariables +var exchange = config.exchangeSettings.exchange; +var asset = config.exchangeSettings.currencyPair.asset; +var currency = config.exchangeSettings.currencyPair.currency; var candleStickSizeMinutes = config.candleStickSizeMinutes; var stopLossEnabled = config.stoplossSettings.enabled; -var initialBalance = config.backTesting.initialBalance; +var initialAssetBalance = config.backTesting.initialAssetBalance; +var initialCurrencyBalance = config.backTesting.initialCurrencyBalance; var slippagePercentage = config.exchangeSettings.slippagePercentage; -var USDBalance = initialBalance; -var BTCBalance = 0; -var initialBalanceBTC = 0; +var USDBalance = initialCurrencyBalance; +var BTCBalance = initialAssetBalance; +var initialBalanceSumInBTC = 0; +var initialBalanceSumInUSD = 0; var totalBalanceInUSD = 0; var totalBalanceInBTC = 0; var profit = 0; @@ -66,7 +71,7 @@ var endDate; //------------------------------AnnounceStart logger.log('------------------------------------------'); -logger.log('Starting BitBot Back-Tester v0.9.0'); +logger.log('Starting BitBot Back-Tester v0.9.1'); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); //------------------------------AnnounceStart @@ -131,16 +136,20 @@ var createOrder = function(type, stopLoss) { exitUSD = USDBalance; - var tradeResult = tools.round(exitUSD - entryUSD, 2); + if(entryUSD > 0) { + + var tradeResult = tools.round(exitUSD - entryUSD, 2); + + if(exitUSD > entryUSD) { + winners += 1; + totalGain = tools.round(totalGain + tradeResult, 2); + if(tradeResult > bigWinner) {bigWinner = tradeResult;} + } else { + losers += 1; + totalLoss = tools.round(totalLoss + tradeResult, 2); + if(tradeResult < bigLoser) {bigLoser = tradeResult;} + } - if(exitUSD > entryUSD) { - winners += 1; - totalGain = tools.round(totalGain + tradeResult, 2); - if(tradeResult > bigWinner) {bigWinner = tradeResult;} - } else { - losers += 1; - totalLoss = tools.round(totalLoss + tradeResult, 2); - if(tradeResult < bigLoser) {bigLoser = tradeResult;} } transactions += 1; @@ -172,7 +181,8 @@ var calculate = function(err, result) { if(loopArray.length > 0) { - initialBalanceBTC = tools.round(USDBalance / _.first(loopArray).close, 2); + initialBalanceSumInBTC = BTCBalance + tools.round(USDBalance / _.first(loopArray).close, 2); + initialBalanceSumInUSD = USDBalance + tools.round(BTCBalance * _.first(loopArray).close, 2); var candleStickSizeSeconds = config.candleStickSizeMinutes * 60; @@ -236,11 +246,11 @@ var report = function(firstCs, lastCs) { totalBalanceInUSD = tools.round(USDBalance + (BTCBalance * lastClose), 2); totalBalanceInBTC = tools.round(BTCBalance + (USDBalance / lastClose), 2); - profit = tools.round(totalBalanceInUSD - initialBalance, 2); - profitPercentage = tools.round(profit / initialBalance * 100, 2); - totalFeeCostsPercentage = tools.round(totalFeeCosts / initialBalance * 100, 2); - bhProfit = tools.round((lastCs.close - firstCs.open) * initialBalanceBTC, 2); - bhProfitPercentage = tools.round(bhProfit / initialBalance * 100, 2); + profit = tools.round(totalBalanceInUSD - initialBalanceSumInUSD, 2); + profitPercentage = tools.round(profit / initialBalanceSumInUSD * 100, 2); + totalFeeCostsPercentage = tools.round(totalFeeCosts / initialBalanceSumInUSD * 100, 2); + bhProfit = tools.round((lastCs.close - firstCs.open) * initialBalanceSumInBTC, 2); + bhProfitPercentage = tools.round(bhProfit / initialBalanceSumInUSD * 100, 2); if(totalBalanceInUSD > highestUSDValue) { highestUSDValue = totalBalanceInUSD; @@ -253,11 +263,16 @@ var report = function(firstCs, lastCs) { averageLoss = tools.round(totalLoss / losers, 2); logger.log('----------Report----------'); + logger.log('Exchange: ' + exchange); logger.log('Transaction Fee: ' + transactionFee + '%'); - logger.log('Initial Balance: ' + initialBalance); - logger.log('Initial Balance BTC: ' + initialBalanceBTC); - logger.log('Final Balance: ' + totalBalanceInUSD); - logger.log('Final Balance BTC: ' + totalBalanceInBTC); + logger.log('Initial ' + asset + ' Balance: ' + initialAssetBalance); + logger.log('Initial ' + currency + ' Balance: ' + initialCurrencyBalance); + logger.log('Final ' + asset + ' Balance: ' + BTCBalance); + logger.log('Final ' + currency + ' Balance: ' + USDBalance); + logger.log('Total Initial Balance in ' + currency + ': ' + initialBalanceSumInUSD); + logger.log('Total Initial Balance in ' + asset + ': ' + initialBalanceSumInBTC); + logger.log('Total Final Balance in ' + currency + ': ' + totalBalanceInUSD); + logger.log('Total Final Balance in ' + asset + ': ' + totalBalanceInBTC); logger.log('Winning trades : ' + winners + ' Losing trades: ' + losers); logger.log('Biggest winner: ' + bigWinner + ' Biggest loser: ' + bigLoser); logger.log('Average winner: ' + averageGain + ' Average loser: ' + averageLoss); @@ -265,7 +280,7 @@ var report = function(firstCs, lastCs) { logger.log('Buy and Hold Profit: ' + bhProfit + ' (' + bhProfitPercentage + '%)'); logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); logger.log('Total traded volue: ' + totalTradedVolume); - logger.log('Highest - Lowest USD Balance: ' + highestUSDValue + ' - ' + lowestUSDValue); + logger.log('Highest - Lowest ' + currency + ' Balance: ' + highestUSDValue + ' - ' + lowestUSDValue); logger.log('Open Price: ' + firstCs.open); logger.log('Close Price: ' + lastCs.close); logger.log('Start - End Date: ' + startDate + ' - ' + endDate); diff --git a/config.sample.js b/config.sample.js index 173c9d2..190d7af 100644 --- a/config.sample.js +++ b/config.sample.js @@ -78,7 +78,8 @@ config.pushOver = { //------------------------------BackTesting config.backTesting = { - initialBalance: 10000 + initialAssetBalance: 0, + initialCurrencyBalance: 10000 }; //------------------------------BackTesting diff --git a/package.json b/package.json index f44f7f0..86d3191 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.9.0", + "version": "0.9.1", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { From bdfe074cb2e9a8c0f9a9499c5d1740d1c5550437 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Thu, 30 Oct 2014 12:19:09 +0100 Subject: [PATCH 33/57] v0.9.2 BTC-E Support --- README.md | 17 ++- app.js | 2 +- backtester.js | 2 +- config.sample.js | 7 +- exchanges/btce.js | 310 ++++++++++++++++++++++++++++++++++++++++++ exchanges/template.js | 2 +- package.json | 3 +- 7 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 exchanges/btce.js diff --git a/README.md b/README.md index b12a3cc..21af91f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ BitBot is a Crypto-Currency trading bot and backtesting platform that connects t BitBot is modular and supports multiple trading strategies / exchanges. +##### Supported Exchanges + +- Bitstamp +- Kraken +- BTC-E + +##### Supported Indicators + +- MACD +- PPO +- PSAR + ## Dependencies - [Node.JS](http://nodejs.org) @@ -44,12 +56,15 @@ Pay close attention to the currencyPair settings: // For Bitstamp just use {pair: 'XBTUSD', asset: 'XBT', currency: 'USD'} // For Kraken look up the currency pairs in their API: https://api.kraken.com/0/public/AssetPairs // Kraken Example: {pair: 'XXBTZEUR', asset: 'XXBT', currency: 'ZEUR'} + // For BTC-E look up the currency pairs in their API: https://btc-e.com/api/3/info + // BTC-E Example: {pair: 'BTC_USD', asset: 'BTC', currency: 'USD'} Then fill in your API details: config.apiSettings = { bitstamp: {clientId: 0, apiKey: '', secret: ''}, - kraken: {apiKey: '', secret: ''} + kraken: {apiKey: '', secret: ''}, + btce: {apiKey: '', secret: ''} }; Make sure you enter the correct connection string for your MongoDB instance: diff --git a/app.js b/app.js index 911f9fd..b1d63df 100644 --- a/app.js +++ b/app.js @@ -170,7 +170,7 @@ var start = function() { //------------------------------AnnounceStart logger.log('------------------------------------------'); - logger.log('Starting BitBot v0.9.1'); + logger.log('Starting BitBot v0.9.2'); logger.log('Real Trading Enabled = ' + config.tradingEnabled); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index b54bf9c..b5474b9 100644 --- a/backtester.js +++ b/backtester.js @@ -71,7 +71,7 @@ var endDate; //------------------------------AnnounceStart logger.log('------------------------------------------'); -logger.log('Starting BitBot Back-Tester v0.9.1'); +logger.log('Starting BitBot Back-Tester v0.9.2'); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); //------------------------------AnnounceStart diff --git a/config.sample.js b/config.sample.js index 190d7af..af8678a 100644 --- a/config.sample.js +++ b/config.sample.js @@ -9,11 +9,13 @@ config.tradingEnabled = false; //------------------------------exchangeSettings config.exchangeSettings = { exchange: '', - // Options: (bitstamp, kraken) + // Options: (bitstamp, kraken, btce) currencyPair: {pair: '', asset: '', currency: ''}, // For Bitstamp just use {pair: 'XBTUSD', asset: 'XBT', currency: 'USD'} // For Kraken look up the currency pairs in their API: https://api.kraken.com/0/public/AssetPairs // Kraken Example: {pair: 'XXBTZEUR', asset: 'XXBT', currency: 'ZEUR'} + // For BTC-E look up the currency pairs in their API: https://btc-e.com/api/3/info + // BTC-E Example: {pair: 'BTC_USD', asset: 'BTC', currency: 'USD'} tradingReserveAsset: 0, // Enter an amount of "asset" you would like to freeze (not trade) tradingReserveCurrency: 0, @@ -25,7 +27,8 @@ config.exchangeSettings = { //------------------------------APISettings config.apiSettings = { bitstamp: {clientId: 0, apiKey: '', secret: ''}, - kraken: {apiKey: '', secret: ''} + kraken: {apiKey: '', secret: ''}, + btce: {apiKey: '', secret: ''} }; //------------------------------APISettings diff --git a/exchanges/btce.js b/exchanges/btce.js new file mode 100644 index 0000000..d0c56d1 --- /dev/null +++ b/exchanges/btce.js @@ -0,0 +1,310 @@ +var _ = require('underscore'); +var async = require('async'); +var btce = require('btc-e'); + +var exchange = function(currencyPair, apiSettings, logger) { + + this.currencyPair = currencyPair; + + this.btce = new btce(apiSettings.apiKey, apiSettings.secret); + + this.q = async.queue(function (task, callback) { + this.logger.debug('Added ' + task.name + ' API call to the queue.'); + this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); + task.func(); + setTimeout(callback,1000); + }.bind(this), 1); + + this.logger = logger; + + _.bindAll(this, 'retry', 'errorHandler', 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); + +}; + +exchange.prototype.retry = function(method, args) { + + var self = this; + + // make sure the callback (and any other fn) + // is bound to api + _.each(args, function(arg, i) { + if(_.isFunction(arg)) + args[i] = _.bind(arg, self); + }); + + // run the failed method again with the same + // arguments after wait + + setTimeout(function() { method.apply(self, args); }, 1000*15); + +}; + +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { + + return function(err, result) { + + var args = _.toArray(receivedArgs); + + var parsedError = null; + + if(err) { + + if(JSON.stringify(err) === '{}' && err.message) { + parsedError = err.message; + } else { + parsedError = JSON.stringify(err); + } + + this.logger.error(callerName + ' Exchange API returned the following error:'); + this.logger.error(parsedError.substring(0,99)); + + if(retryAllowed) { + this.logger.error('Retrying in 15 seconds!'); + return this.retry(caller, args); + } + + } else { + + this.logger.debug(callerName + ' Exchange API Call Result (Substring)!'); + this.logger.debug(JSON.stringify(result).substring(0,99)); + + } + + handler(parsedError, result); + + }.bind(this); + +}; + +exchange.prototype.getTrades = function(retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var pair = this.currencyPair.pair.toLowerCase(); + + var handler = function(err, response) { + + if(!err) { + + var trades = _.map(response.reverse(), function(entry) { + + return {date: parseInt(entry.date), price: parseFloat(entry.price), amount: parseFloat(entry.amount)}; + + }); + + cb(null, trades); + + } else { + + cb(err, null); + + } + + }; + + this.btce.trades(pair, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); + + }.bind(this); + + this.q.push({name: 'getTrades', func: wrapper}); + +}; + +exchange.prototype.getBalance = function(retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var asset = this.currencyPair.asset.toLowerCase(); + var currency = this.currencyPair.currency.toLowerCase(); + + var handler = function(err, response) { + + if(!err) { + + cb(null, {assetAvailable: response.funds[asset], currencyAvailable: response.funds[currency], fee: 0.2}); + + } else { + + cb(err, null); + + } + + }; + + this.btce.getInfo(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); + + }.bind(this); + + this.q.push({name: 'getBalance', func: wrapper}); + +}; + +exchange.prototype.getOrderBook = function(retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var pair = this.currencyPair.pair.toLowerCase(); + + var handler = function(err, response) { + + if(!err) { + + var bids = _.map(response.bids, function(bid) { + return {assetAmount: bid[1], currencyPrice: bid[0]}; + }); + + var asks = _.map(response.asks, function(ask) { + return {assetAmount: ask[1], currencyPrice: ask[0]}; + }); + + cb(null, {bids: bids, asks: asks}); + + } else { + + cb(err, null); + + } + + }; + + this.btce.depth(pair, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); + + }.bind(this); + + this.q.push({name: 'getOrderBook', func: wrapper}); + +}; + +exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var pair = this.currencyPair.pair.toLowerCase(); + + var handler = function(err, response) { + + if(!err) { + + cb(null, {txid: response.order_id}); + + } else { + + cb(err, null); + + } + + }; + + if(type === 'buy') { + + this.btce.trade(pair, 'buy', price, amount, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + + } else if (type === 'sell') { + + this.btce.trade(pair, 'sell', price, amount, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + + } else { + + cb(new Error('Invalid order type!'), null); + + } + + }.bind(this); + + this.q.push({name: 'placeOrder', func: wrapper}); + +}; + +exchange.prototype.orderFilled = function(order, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + var handler = function(err, response) { + + if(!err) { + + if(response[order]) { + + cb(null, false); + + } else { + + cb(null, true); + + } + + } else { + + cb(err, null); + + } + + }; + + this.btce.orderInfo(order, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); + + }.bind(this); + + this.q.push({name: 'orderFilled', func: wrapper}); + +}; + +exchange.prototype.cancelOrder = function(order, retry, cb) { + + var args = arguments; + + var wrapper = function() { + + this.orderFilled(order, retry, function(err, filled) { + + if(!filled && !err) { + + var handler = function(err, response) { + + if(!err) { + + if(response.order_id === order) { + cb(null, true); + } else { + cb(null, false); + } + + } else { + + cb(err, null); + + } + + }; + + this.btce.cancelOrder(order, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); + + } else if(filled && !err) { + + cb(null, false); + + } else { + + cb(err, null); + + } + + }.bind(this)); + + }.bind(this); + + this.q.push({name: 'cancelOrder', func: wrapper}); + +}; + +module.exports = exchange; diff --git a/exchanges/template.js b/exchanges/template.js index 1dc9ac6..77e936e 100644 --- a/exchanges/template.js +++ b/exchanges/template.js @@ -88,7 +88,7 @@ exchange.prototype.getTrades = function(retry, cb) { var handler = function(err, response) { - cb(null, {date: timestamp, price: number, amount: number}); + cb(null, [{date: timestamp, price: number, amount: number}]); }; diff --git a/package.json b/package.json index 86d3191..8d56ca7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.9.1", + "version": "0.9.2", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { @@ -14,6 +14,7 @@ "underscore": "1.6.0", "ws": "0.4.31", "kraken-exchange-api": "0.1.0", + "btc-e": "1.0.2", "winston": "0.8.0" }, "scripts": { From 55032c39cc9b863bee4a3483317624a068437cfd Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Thu, 6 Nov 2014 20:48:39 +0100 Subject: [PATCH 34/57] Updated .gitignore --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 496ee2c..b3d0573 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ -.DS_Store \ No newline at end of file +# OSX +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ From 1f26a4f5bf5f67ff50425b3ca6bc591377e23f45 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Fri, 7 Nov 2014 00:32:46 +0100 Subject: [PATCH 35/57] v0.9.3 Kraken API bugfix and more in details - exchanges/kraken.js was still using the old API module - Restructured the config.sample.js file - Improved performance of aggregating candlesticks --- app.js | 8 ++++---- backtester.js | 16 ++++++++-------- config.sample.js | 15 ++++++--------- exchanges/kraken.js | 2 +- package.json | 2 +- services/storage.js | 37 ++++++++++++++++++++++++++----------- services/tradingadvisor.js | 4 ++-- util/tools.js | 13 +++++++++++++ 8 files changed, 61 insertions(+), 36 deletions(-) diff --git a/app.js b/app.js index b1d63df..d817b50 100644 --- a/app.js +++ b/app.js @@ -23,13 +23,13 @@ var storage = new storageservice(config.exchangeSettings, config.mongoConnection var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); var processor = new dataprocessor(storage, logger); -var aggregator = new candleaggregator(config.candleStickSizeMinutes, storage, logger); -var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, false, storage, logger); +var aggregator = new candleaggregator(config.indicatorSettings.candleStickSizeMinutes, storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, false, storage, logger); var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings, storage, exchangeapi, logger); var pusher = new pushservice(config.pushOver, logger); var monitor = new ordermonitor(exchangeapi, logger); var reporter = new profitreporter(config.exchangeSettings.currencyPair, storage, exchangeapi, logger); -var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes, storage, logger); +var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.indicatorSettings.candleStickSizeMinutes, storage, logger); //------------------------------IntializeModules retriever.on('update', function(ticks){ @@ -170,7 +170,7 @@ var start = function() { //------------------------------AnnounceStart logger.log('------------------------------------------'); - logger.log('Starting BitBot v0.9.2'); + logger.log('Starting BitBot v0.9.3'); logger.log('Real Trading Enabled = ' + config.tradingEnabled); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index b5474b9..a08b63f 100644 --- a/backtester.js +++ b/backtester.js @@ -19,15 +19,15 @@ var logger = new loggingservice('backtester', config.debug); var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); var processor = new dataprocessor(storage, logger); -var advisor = new tradingadvisor(config.indicatorSettings, config.candleStickSizeMinutes, true, storage, logger); -var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.candleStickSizeMinutes, storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, true, storage, logger); +var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.indicatorSettings.candleStickSizeMinutes, storage, logger); //------------------------------IntializeModules //------------------------------IntializeVariables var exchange = config.exchangeSettings.exchange; var asset = config.exchangeSettings.currencyPair.asset; var currency = config.exchangeSettings.currencyPair.currency; -var candleStickSizeMinutes = config.candleStickSizeMinutes; +var candleStickSizeMinutes = config.indicatorSettings.candleStickSizeMinutes; var stopLossEnabled = config.stoplossSettings.enabled; var initialAssetBalance = config.backTesting.initialAssetBalance; var initialCurrencyBalance = config.backTesting.initialCurrencyBalance; @@ -71,7 +71,7 @@ var endDate; //------------------------------AnnounceStart logger.log('------------------------------------------'); -logger.log('Starting BitBot Back-Tester v0.9.2'); +logger.log('Starting BitBot Back-Tester v0.9.3'); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); //------------------------------AnnounceStart @@ -184,7 +184,7 @@ var calculate = function(err, result) { initialBalanceSumInBTC = BTCBalance + tools.round(USDBalance / _.first(loopArray).close, 2); initialBalanceSumInUSD = USDBalance + tools.round(BTCBalance * _.first(loopArray).close, 2); - var candleStickSizeSeconds = config.candleStickSizeMinutes * 60; + var candleStickSizeSeconds = candleStickSizeMinutes * 60; if(csArray.length > 0) { @@ -214,7 +214,7 @@ var calculate = function(err, result) { pricemon.update(candle, function(err) { - logger.debug('Backtest: Created a new ' + config.candleStickSizeMinutes + ' minute candlestick!'); + logger.debug('Backtest: Created a new ' + candleStickSizeMinutes + ' minute candlestick!'); logger.debug(JSON.stringify(candle)); advisor.update(candle); @@ -222,7 +222,7 @@ var calculate = function(err, result) { } else { - logger.debug('Backtest: Created a new ' + config.candleStickSizeMinutes + ' minute candlestick!'); + logger.debug('Backtest: Created a new ' + candleStickSizeMinutes + ' minute candlestick!'); logger.debug(JSON.stringify(candle)); advisor.update(candle); @@ -295,7 +295,7 @@ var start = function() { { balance: function(cb) {exchangeapi.getBalance(true, cb);}, dbCandleSticks: function(cb) {storage.getAllCandlesSince(0, cb);}, - aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(config.candleStickSizeMinutes, cb);} + aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(candleStickSizeMinutes, cb);} }, calculate ); diff --git a/config.sample.js b/config.sample.js index af8678a..fed4bf3 100644 --- a/config.sample.js +++ b/config.sample.js @@ -45,23 +45,20 @@ config.downloaderRefreshSeconds = 10; // Best to keep this default setting unless you know what you are doing //------------------------------downloaderSettings -//------------------------------candleStickSizeSettings -config.candleStickSizeMinutes = 5; -//------------------------------candleStickSizeSettings - -//------------------------------orderSettings -config.orderKeepAliveMinutes = config.candleStickSizeMinutes / 10; -//------------------------------orderSettings - //------------------------------IndicatorSettings config.indicatorSettings = { indicator: 'MACD', // Choices: Any indicator from the indicators folder - options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0} + options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0}, // Options needed for your indicator (Look them up in the indicator's file) + candleStickSizeMinutes: 5 }; //------------------------------IndicatorSettings +//------------------------------orderSettings +config.orderKeepAliveMinutes = config.indicatorSettings.candleStickSizeMinutes / 10; +//------------------------------orderSettings + //------------------------------stopLossSettings config.stoplossSettings = { enabled: false, diff --git a/exchanges/kraken.js b/exchanges/kraken.js index b9a6204..3c451bd 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -1,6 +1,6 @@ var _ = require('underscore'); var async = require('async'); -var Kraken = require('kraken-api'); +var Kraken = require('kraken-exchange-api'); var exchange = function(currencyPair, apiSettings, logger) { diff --git a/package.json b/package.json index 8d56ca7..0d47084 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.9.2", + "version": "0.9.3", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/storage.js b/services/storage.js index 5e3adf5..52325e6 100644 --- a/services/storage.js +++ b/services/storage.js @@ -291,28 +291,43 @@ storage.prototype.aggregateCandleSticks = function(candleStickSize, candleSticks var aggregatedCandleSticks = []; var startTimeStamp = Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds; + var beginPeriod = startTimeStamp; + var endPeriod = startTimeStamp + candleStickSizeSeconds; var stopTimeStamp = _.last(candleSticks).period; - var filterOnPeriod = function(candleStick) { return candleStick.period >= beginPeriod && candleStick.period < endPeriod; }; + var relevantSticks = []; + var filterOnVolume = function(candleStick) { return candleStick.volume > 0; }; - for(var i = startTimeStamp;i <= stopTimeStamp;i = i + candleStickSizeSeconds) { + _.each(candleSticks, function(candleStick) { - var beginPeriod = i; - var endPeriod = beginPeriod + candleStickSizeSeconds; + if(candleStick.period >= beginPeriod && candleStick.period < endPeriod) { - var relevantSticks = _.filter(candleSticks, filterOnPeriod); + relevantSticks.push(candleStick); - var vrelevantSticks = _.filter(relevantSticks, filterOnVolume); + } else { - if(vrelevantSticks.length > 0) { - relevantSticks = vrelevantSticks; - } + var vrelevantSticks = _.filter(relevantSticks, filterOnVolume); + + if(vrelevantSticks.length > 0) { + relevantSticks = vrelevantSticks; + } + + aggregatedCandleSticks.push(this.calculateAggregatedCandleStick(beginPeriod, relevantSticks)); - var currentCandleStick = this.calculateAggregatedCandleStick(beginPeriod, relevantSticks); + beginPeriod = endPeriod; + endPeriod = endPeriod + candleStickSizeSeconds; - aggregatedCandleSticks.push(currentCandleStick); + relevantSticks = []; + + relevantSticks.push(candleStick); + + } + + }.bind(this)); + if(relevantSticks.length > 0) { + aggregatedCandleSticks.push(this.calculateAggregatedCandleStick(beginPeriod, relevantSticks)); } return aggregatedCandleSticks; diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index d3a17e7..edec482 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -2,9 +2,9 @@ var _ = require('underscore'); var async = require('async'); var fs = require('fs'); -var advisor = function(indicatorSettings, candleStickSize, backTesting, storage, logger) { +var advisor = function(indicatorSettings, backTesting, storage, logger) { - this.candleStickSize = candleStickSize; + this.candleStickSize = indicatorSettings.candleStickSizeMinutes; this.backTesting = backTesting; this.storage = storage; this.logger = logger; diff --git a/util/tools.js b/util/tools.js index fb12632..fb8ca76 100644 --- a/util/tools.js +++ b/util/tools.js @@ -23,6 +23,19 @@ tools.prototype.floor = function(value, decimals) { return Number(Math.floor(value+'e'+decimals)+'e-'+decimals); }; +tools.prototype.rangeToArray = function(range) { + + var result = []; + var increment = this.floor(1 / Math.pow(10,range[0]), range[0]); + + for(var i = range[1]; i <= range[2]; i = this.round(i + increment,range[0])) { + result.push(i); + } + + return result; + +} + var utiltools = new tools(); module.exports = utiltools; From ce66e3a797850ff271e6728b8e189691e66e83a8 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 30 Nov 2014 00:55:53 +0100 Subject: [PATCH 36/57] BTC-E Exchange bug fix Fixed a bug that could cause the order monitor not to realize that an order was filled. --- exchanges/bitstamp.js | 2 +- exchanges/btce.js | 10 +++++++++- exchanges/kraken.js | 2 +- exchanges/template.js | 2 +- services/ordermonitor.js | 2 +- services/tradingagent.js | 4 ++++ 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index dcbc4c0..4c8d02e 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -199,7 +199,7 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { if(!result.error) { - cb(null, {txid: result.id}); + cb(null, {txid: result.id, status: 'open'}); } else { diff --git a/exchanges/btce.js b/exchanges/btce.js index d0c56d1..6f3418b 100644 --- a/exchanges/btce.js +++ b/exchanges/btce.js @@ -193,7 +193,15 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { if(!err) { - cb(null, {txid: response.order_id}); + var status = 'open'; + + if(response.order_id === 0) { + + status = 'filled'; + + } + + cb(null, {txid: response.order_id, status: status}); } else { diff --git a/exchanges/kraken.js b/exchanges/kraken.js index 3c451bd..3ee1545 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -256,7 +256,7 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { if(!err) { - cb(null, {txid: data.result.txid[0]}); + cb(null, {txid: data.result.txid[0], status: 'open'}); } else { diff --git a/exchanges/template.js b/exchanges/template.js index 77e936e..858e0a4 100644 --- a/exchanges/template.js +++ b/exchanges/template.js @@ -151,7 +151,7 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var handler = function(err, response) { - cb(null, {txid: transaction_id}); + cb(null, {txid: transaction_id, status: 'open'}); }; diff --git a/services/ordermonitor.js b/services/ordermonitor.js index b0eb965..8ab47c6 100644 --- a/services/ordermonitor.js +++ b/services/ordermonitor.js @@ -78,7 +78,7 @@ monitor.prototype.add = function(orderDetails, cancelTime) { this.logger.log('Monitoring order: ' + this.checkOrder.id + ' (Cancellation after ' + cancelTime + ' minutes)'); - if(this.checkOrder.id === 'Simulated') { + if(this.checkOrder.status === 'filled') { this.processSimulation(this.checkOrder); diff --git a/services/tradingagent.js b/services/tradingagent.js index 9aaba46..6541b61 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -136,6 +136,8 @@ agent.prototype.placeSimulatedOrder = function() { this.orderDetails.order = 'Simulated'; + this.orderDetails.status = order.status; + this.logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); this.emit('simulatedOrder', this.orderDetails); @@ -154,6 +156,8 @@ agent.prototype.processOrder = function(err, order) { this.orderDetails.order = order.txid; + this.orderDetails.status = order.status; + this.logger.log('Placed ' + this.orderDetails.orderType + ' order: ' + this.orderDetails.order + ' (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); this.emit('realOrder', this.orderDetails); From 4d30178e7abaa570c4eecd2bc2ac26f4f031e8ab Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 30 Nov 2014 11:23:20 +0100 Subject: [PATCH 37/57] Fixed a bug in the order monitor --- services/ordermonitor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ordermonitor.js b/services/ordermonitor.js index 8ab47c6..b855f03 100644 --- a/services/ordermonitor.js +++ b/services/ordermonitor.js @@ -74,7 +74,7 @@ monitor.prototype.add = function(orderDetails, cancelTime) { var wrapper = function() { - this.checkOrder = {id:orderDetails.order, orderDetails:orderDetails, status:'open'}; + this.checkOrder = {id: orderDetails.order, orderDetails: orderDetails, status: orderDetails.status}; this.logger.log('Monitoring order: ' + this.checkOrder.id + ' (Cancellation after ' + cancelTime + ' minutes)'); From 86d516d268f34e25b37a192473a1e0e6b4dfc323 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 7 Dec 2014 23:16:38 +0100 Subject: [PATCH 38/57] Fixed a bug for all Exchange wrappers - Made sure cancelOrder executed it's steps correctly - Modified the fastest polling rate to 1 request every 2 seconds --- exchanges/bitstamp.js | 48 ++++++++++++++++++++++++++++--------------- exchanges/btce.js | 24 +++++++++++----------- exchanges/kraken.js | 24 +++++++++++----------- exchanges/template.js | 2 +- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 4c8d02e..15af9ea 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -12,7 +12,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); task.func(); - setTimeout(callback,1000); + setTimeout(callback,2000); }.bind(this), 1); this.logger = logger; @@ -281,31 +281,47 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + this.orderFilled(order, retry, function(err, filled) { - var handler = function(err, result) { + if(!filled && !err) { - if(!err) { + var wrapper = function() { - if(!result.error) { - cb(null, true); - } else { - cb(null, false); - } + var handler = function(err, result) { - } else { + if(!err) { - cb(err, null); + if(!result.error) { + cb(null, true); + } else { + cb(null, false); + } - } + } else { - }; + cb(err, null); - this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); + } - }.bind(this); + }; + + this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); + + }.bind(this); + + this.q.push({name: 'cancelOrder', func: wrapper}); + + } else if(filled && !err) { + + cb(null, false); + + } else { + + cb(err, null); + + } - this.q.push({name: 'cancelOrder', func: wrapper}); + }.bind(this)); }; diff --git a/exchanges/btce.js b/exchanges/btce.js index 6f3418b..2c4bf10 100644 --- a/exchanges/btce.js +++ b/exchanges/btce.js @@ -12,7 +12,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); task.func(); - setTimeout(callback,1000); + setTimeout(callback,2000); }.bind(this), 1); this.logger = logger; @@ -271,11 +271,11 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + this.orderFilled(order, retry, function(err, filled) { - this.orderFilled(order, retry, function(err, filled) { + if(!filled && !err) { - if(!filled && !err) { + var wrapper = function() { var handler = function(err, response) { @@ -297,21 +297,21 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { this.btce.cancelOrder(order, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); - } else if(filled && !err) { + }.bind(this); - cb(null, false); + this.q.push({name: 'cancelOrder', func: wrapper}); - } else { + } else if(filled && !err) { - cb(err, null); + cb(null, false); - } + } else { - }.bind(this)); + cb(err, null); - }.bind(this); + } - this.q.push({name: 'cancelOrder', func: wrapper}); + }.bind(this)); }; diff --git a/exchanges/kraken.js b/exchanges/kraken.js index 3ee1545..39e352c 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -12,7 +12,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); task.func(); - setTimeout(callback,1000); + setTimeout(callback,2000); }.bind(this), 1); this.logger = logger; @@ -332,11 +332,11 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + this.orderFilled(order, retry, function(err, filled) { - this.orderFilled(order, retry, function(err, filled) { + if(!filled && !err) { - if(!filled && !err) { + var wrapper = function() { var handler = function(err, data) { @@ -358,21 +358,21 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); - } else if(filled && !err) { + }.bind(this); - cb(null, false); + this.q.push({name: 'cancelOrder', func: wrapper}); - } else { + } else if(filled && !err) { - cb(err, null); + cb(null, false); - } + } else { - }.bind(this)); + cb(err, null); - }.bind(this); + } - this.q.push({name: 'cancelOrder', func: wrapper}); + }.bind(this)); }; diff --git a/exchanges/template.js b/exchanges/template.js index 858e0a4..e01f4bc 100644 --- a/exchanges/template.js +++ b/exchanges/template.js @@ -16,7 +16,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); task.func(); - setTimeout(callback,1000); + setTimeout(callback,2000); }.bind(this), 1); this.logger = logger; From dfa9d959171c7ab337a82f6781246edb13889e60 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 8 Dec 2014 17:44:25 +0100 Subject: [PATCH 39/57] v0.9.4 Support for numbers up to 8 decimals --- app.js | 2 +- backtester.js | 56 +++++++++++++++++++------------------- indicators/MACD.js | 2 +- indicators/PPO.js | 2 +- indicators/PSAR.js | 10 +++---- package.json | 2 +- services/dataprocessor.js | 2 +- services/profitreporter.js | 10 +++---- services/storage.js | 2 +- services/tradingagent.js | 28 +++---------------- util/tools.js | 18 +++++++++--- 11 files changed, 62 insertions(+), 72 deletions(-) diff --git a/app.js b/app.js index d817b50..ad9254b 100644 --- a/app.js +++ b/app.js @@ -170,7 +170,7 @@ var start = function() { //------------------------------AnnounceStart logger.log('------------------------------------------'); - logger.log('Starting BitBot v0.9.3'); + logger.log('Starting BitBot v0.9.4'); logger.log('Real Trading Enabled = ' + config.tradingEnabled); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); diff --git a/backtester.js b/backtester.js index a08b63f..8576c9b 100644 --- a/backtester.js +++ b/backtester.js @@ -71,7 +71,7 @@ var endDate; //------------------------------AnnounceStart logger.log('------------------------------------------'); -logger.log('Starting BitBot Back-Tester v0.9.3'); +logger.log('Starting BitBot Back-Tester v0.9.4'); logger.log('Working Dir = ' + process.cwd()); logger.log('------------------------------------------'); //------------------------------AnnounceStart @@ -80,22 +80,22 @@ var createOrder = function(type, stopLoss) { var usableBalance = 0; - if(type === 'buy' && USDBalance !== 0) { + if(type === 'buy' && USDBalance > 0) { entryUSD = USDBalance; - usableBalance = USDBalance * (1 - (transactionFee / 100)); + usableBalance = tools.round(USDBalance * (1 - (transactionFee / 100)), 8); - lastClosePlusSlippage = tools.round(lastClose * (1 + (slippagePercentage / 100)), 2); + lastClosePlusSlippage = tools.round(lastClose * (1 + (slippagePercentage / 100)), 8); - totalTradedVolume = tools.round(totalTradedVolume + usableBalance, 2); + totalTradedVolume = tools.round(totalTradedVolume + usableBalance, 8); - totalFeeCosts = tools.round(totalFeeCosts + (USDBalance * (transactionFee / 100)), 2); + totalFeeCosts = tools.round(totalFeeCosts + (USDBalance * (transactionFee / 100)), 8); - BTCBalance = tools.round(BTCBalance + (usableBalance / lastClosePlusSlippage), 2); + BTCBalance = tools.round(BTCBalance + (usableBalance / lastClosePlusSlippage), 8); USDBalance = 0; - var newUSDBalance = tools.round(BTCBalance * lastClosePlusSlippage, 2); + var newUSDBalance = tools.round(BTCBalance * lastClosePlusSlippage, 8); if(newUSDBalance > highestUSDValue) { highestUSDValue = newUSDBalance; @@ -115,17 +115,17 @@ var createOrder = function(type, stopLoss) { pricemon.setPosition('bought', lastClosePlusSlippage); advisor.setPosition({pos: 'bought', price: lastClosePlusSlippage}); - } else if(type === 'sell' && BTCBalance !== 0) { + } else if(type === 'sell' && BTCBalance > 0) { - usableBalance = BTCBalance * (1 - (transactionFee / 100)); + usableBalance = tools.round(BTCBalance * (1 - (transactionFee / 100)), 8); - lastCloseMinusSlippage = tools.round(lastClose * (1 - (slippagePercentage / 100)), 2); + lastCloseMinusSlippage = tools.round(lastClose * (1 - (slippagePercentage / 100)), 8); - totalTradedVolume = tools.round(totalTradedVolume + (usableBalance * lastCloseMinusSlippage), 2); + totalTradedVolume = tools.round(totalTradedVolume + (usableBalance * lastCloseMinusSlippage), 8); - totalFeeCosts = tools.round(totalFeeCosts + (BTCBalance * (transactionFee / 100) * lastCloseMinusSlippage), 2); + totalFeeCosts = tools.round(totalFeeCosts + (BTCBalance * (transactionFee / 100) * lastCloseMinusSlippage), 8); - USDBalance = tools.round(USDBalance + (usableBalance * lastCloseMinusSlippage), 2); + USDBalance = tools.round(USDBalance + (usableBalance * lastCloseMinusSlippage), 8); BTCBalance = 0; if(USDBalance > highestUSDValue) { @@ -138,15 +138,15 @@ var createOrder = function(type, stopLoss) { if(entryUSD > 0) { - var tradeResult = tools.round(exitUSD - entryUSD, 2); + var tradeResult = tools.round(exitUSD - entryUSD, 8); if(exitUSD > entryUSD) { winners += 1; - totalGain = tools.round(totalGain + tradeResult, 2); + totalGain = tools.round(totalGain + tradeResult, 8); if(tradeResult > bigWinner) {bigWinner = tradeResult;} } else { losers += 1; - totalLoss = tools.round(totalLoss + tradeResult, 2); + totalLoss = tools.round(totalLoss + tradeResult, 8); if(tradeResult < bigLoser) {bigLoser = tradeResult;} } @@ -181,8 +181,8 @@ var calculate = function(err, result) { if(loopArray.length > 0) { - initialBalanceSumInBTC = BTCBalance + tools.round(USDBalance / _.first(loopArray).close, 2); - initialBalanceSumInUSD = USDBalance + tools.round(BTCBalance * _.first(loopArray).close, 2); + initialBalanceSumInBTC = BTCBalance + tools.round(USDBalance / _.first(loopArray).close, 8); + initialBalanceSumInUSD = USDBalance + tools.round(BTCBalance * _.first(loopArray).close, 8); var candleStickSizeSeconds = candleStickSizeMinutes * 60; @@ -244,13 +244,13 @@ var calculate = function(err, result) { var report = function(firstCs, lastCs) { - totalBalanceInUSD = tools.round(USDBalance + (BTCBalance * lastClose), 2); - totalBalanceInBTC = tools.round(BTCBalance + (USDBalance / lastClose), 2); - profit = tools.round(totalBalanceInUSD - initialBalanceSumInUSD, 2); - profitPercentage = tools.round(profit / initialBalanceSumInUSD * 100, 2); - totalFeeCostsPercentage = tools.round(totalFeeCosts / initialBalanceSumInUSD * 100, 2); - bhProfit = tools.round((lastCs.close - firstCs.open) * initialBalanceSumInBTC, 2); - bhProfitPercentage = tools.round(bhProfit / initialBalanceSumInUSD * 100, 2); + totalBalanceInUSD = tools.round(USDBalance + (BTCBalance * lastClose), 8); + totalBalanceInBTC = tools.round(BTCBalance + (USDBalance / lastClose), 8); + profit = tools.round(totalBalanceInUSD - initialBalanceSumInUSD, 8); + profitPercentage = tools.round(profit / initialBalanceSumInUSD * 100, 8); + totalFeeCostsPercentage = tools.round(totalFeeCosts / initialBalanceSumInUSD * 100, 8); + bhProfit = tools.round((lastCs.close - firstCs.open) * initialBalanceSumInBTC, 8); + bhProfitPercentage = tools.round(bhProfit / initialBalanceSumInUSD * 100, 8); if(totalBalanceInUSD > highestUSDValue) { highestUSDValue = totalBalanceInUSD; @@ -259,8 +259,8 @@ var report = function(firstCs, lastCs) { startDate = moment(new Date(firstCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); endDate = moment(new Date(lastCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); - averageGain = tools.round(totalGain / winners, 2); - averageLoss = tools.round(totalLoss / losers, 2); + averageGain = tools.round(totalGain / winners, 8); + averageLoss = tools.round(totalLoss / losers, 8); logger.log('----------Report----------'); logger.log('Exchange: ' + exchange); diff --git a/indicators/MACD.js b/indicators/MACD.js index 8845a1f..8e4d291 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -44,7 +44,7 @@ indicator.prototype.calculate = function(cs) { var macd = emaShort - emaLong; var macdSignal = calculateEma(this.options.emaPeriods, macd, this.previousIndicator.macdSignal); - var macdHistogram = tools.round(macd - macdSignal, 2); + var macdHistogram = tools.round(macd - macdSignal, 8); this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'macd': macd, 'macdSignal': macdSignal, 'result': macdHistogram}; diff --git a/indicators/PPO.js b/indicators/PPO.js index a980573..a34579b 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -44,7 +44,7 @@ indicator.prototype.calculate = function(cs) { var PPO = ((emaShort - emaLong) / emaLong) * 100; var PPOSignal = calculateEma(this.options.emaPeriods, PPO, this.previousIndicator.PPOSignal); - var PPOHistogram = tools.round(PPO - PPOSignal, 2); + var PPOHistogram = tools.round(PPO - PPOSignal, 8); this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'PPO': PPO, 'PPOSignal': PPOSignal, 'result': PPOHistogram}; diff --git a/indicators/PSAR.js b/indicators/PSAR.js index 9e2afa5..e631ebd 100644 --- a/indicators/PSAR.js +++ b/indicators/PSAR.js @@ -23,10 +23,10 @@ var calculatePSAR = function(previousPSAR, previousEP, previousAF, previousTrend var temp; if(previousTrend === 1) { - temp = tools.round(previousPSAR + (previousAF * (previousEP - previousPSAR)), 2); + temp = tools.round(previousPSAR + (previousAF * (previousEP - previousPSAR)), 8); PSAR = _.min([temp, limit]); } else if (previousTrend === -1) { - temp = tools.round(previousPSAR - (previousAF * (previousPSAR - previousEP)), 2); + temp = tools.round(previousPSAR - (previousAF * (previousPSAR - previousEP)), 8); PSAR = _.max([temp,limit]); } @@ -93,7 +93,7 @@ var calculateAF = function(EP, previousEP, trend, previousTrend, previousAF, AFI if(EP !== previousEP) {EPChanged = true;} if(EPChanged && trend === previousTrend && previousAF < maximumAF) { - AF = tools.round(previousAF + AFIncrement, 2); + AF = tools.round(previousAF + AFIncrement, 8); } else if (trend !== previousTrend) { AF = AFIncrement; } else { @@ -116,8 +116,8 @@ indicator.prototype.calculate = function(cs) { } if(!this.firstCandleDone) { - this.previousPSAR = tools.round(cs.low, 2); - this.previousEP = tools.round(cs.high, 2); + this.previousPSAR = tools.round(cs.low, 8); + this.previousEP = tools.round(cs.high, 8); this.previousAF = this.options.AFIncrement; this.previousTrend = 1; this.firstCandleDone = true; diff --git a/package.json b/package.json index 0d47084..71a1258 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.9.3", + "version": "0.9.4", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/dataprocessor.js b/services/dataprocessor.js index 2fab125..ac5e494 100644 --- a/services/dataprocessor.js +++ b/services/dataprocessor.js @@ -38,7 +38,7 @@ processor.prototype.updateCandleStick = function (candleStick, tick) { candleStick.low = _.min([candleStick.low, tick.price]); candleStick.volume = tools.round(candleStick.volume + tick.amount, 8); - candleStick.vwap = tools.round((currentVwap + newVwap) / candleStick.volume, 2); + candleStick.vwap = tools.round((currentVwap + newVwap) / candleStick.volume, 8); } diff --git a/services/profitreporter.js b/services/profitreporter.js index f5467de..db14b9d 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -22,12 +22,12 @@ Util.inherits(reporter, EventEmitter); reporter.prototype.intialize = function(err, result) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 2); + this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 8); this.highestBid = _.first(result.orderBook.bids).currencyPrice; this.assetBalanceInCurrency = this.assetBalance * this.highestBid; - this.initalTotalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 2); + this.initalTotalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 8); this.storage.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { @@ -64,14 +64,14 @@ reporter.prototype.createReport = function() { reporter.prototype.processBalance = function(err, result) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 2); + this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 8); this.highestBid = _.first(result.orderBook.bids).currencyPrice; this.assetBalanceInCurrency = this.assetBalance * this.highestBid; - this.totalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 2); + this.totalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 8); this.profitAbsolute = this.totalCurrencyBalance - this.initalTotalCurrencyBalance; - this.profitPercentage = tools.round((this.profitAbsolute / this.initalTotalCurrencyBalance) * 100, 2); + this.profitPercentage = tools.round((this.profitAbsolute / this.initalTotalCurrencyBalance) * 100, 8); if(this.includeReport) { this.createReport(); diff --git a/services/storage.js b/services/storage.js index 52325e6..92614a8 100644 --- a/services/storage.js +++ b/services/storage.js @@ -277,7 +277,7 @@ storage.prototype.calculateAggregatedCandleStick = function(period, relevantStic if(currentCandleStick.volume === 0) { currentCandleStick.vwap = currentCandleStick.close; } else { - currentCandleStick.vwap = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + (entry.vwap * entry.volume); }, 0) / currentCandleStick.volume, 2); + currentCandleStick.vwap = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + (entry.vwap * entry.volume); }, 0) / currentCandleStick.volume, 8); } return currentCandleStick; diff --git a/services/tradingagent.js b/services/tradingagent.js index 6541b61..f8ac00c 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -62,51 +62,31 @@ agent.prototype.calculateOrder = function(result) { var orderBook = result.orderBook; var lastClose = result.lastClose; - var minClose = tools.round(lastClose * 0.9975, 2); - var maxClose = tools.round(lastClose * 1.0025, 2); this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.tradingFee +')'); if(this.orderDetails.orderType === 'buy') { - //var lowestAsk = _.first(orderBook.asks)[0]; - /*var lowestAsk = _.min(orderBook.asks, function(ask){ return parseFloat(ask[0]); })[0]; - - if(lowestAsk < minClose) { - lowestAsk = minClose; - } else if(lowestAsk > lastClose) { - lowestAsk = lastClose; - }*/ - var lowestAsk = lastClose; - var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 2); + var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 8); var balance = (this.orderDetails.currencyBalance - this.tradingReserveCurrency) * (1 - (this.orderDetails.tradingFee / 100)); this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); this.orderDetails.price = lowestAskWithSlippage; - this.orderDetails.amount = tools.round((balance / this.orderDetails.price) - 0.005, 2); + this.orderDetails.amount = tools.floor(balance / this.orderDetails.price, 8); } else if(this.orderDetails.orderType === 'sell') { - //var highestBid = _.first(orderBook.bids)[0]; - /*var highestBid = _.max(orderBook.bids, function(bid){ return parseFloat(bid[0]); })[0]; - - if(highestBid > maxClose) { - highestBid = maxClose; - } else if(highestBid < lastClose) { - highestBid = lastClose; - }*/ - var highestBid = lastClose; - var highestBidWithSlippage = tools.round(highestBid * (1 - (this.slippagePercentage / 100)), 2); + var highestBidWithSlippage = tools.round(highestBid * (1 - (this.slippagePercentage / 100)), 8); this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); this.orderDetails.price = highestBidWithSlippage; - this.orderDetails.amount = tools.round(this.orderDetails.assetBalance - this.tradingReserveAsset, 2); + this.orderDetails.amount = tools.round(this.orderDetails.assetBalance - this.tradingReserveAsset, 8); } diff --git a/util/tools.js b/util/tools.js index fb8ca76..e5521fa 100644 --- a/util/tools.js +++ b/util/tools.js @@ -8,19 +8,29 @@ tools.prototype.unixTimeStamp = function(timestamp) { }; tools.prototype.getRandomInt = function(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; + return Math.floor(Math.random() * (max - min + 1)) + min; }; tools.prototype.getRandomArbitrary = function(decimals, min, max) { - return (Math.random() * (max - min) + min).toFixed(decimals); + return (Math.random() * (max - min) + min).toFixed(decimals); }; tools.prototype.round = function(value, decimals) { - return Number(Math.round(value+'e'+decimals)+'e-'+decimals); + // Shift + value = value.toString().split('e'); + value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] + decimals) : decimals))); + // Shift back + value = value.toString().split('e'); + return Number((value[0] + 'e' + (value[1] ? (+value[1] - decimals) : -decimals))); }; tools.prototype.floor = function(value, decimals) { - return Number(Math.floor(value+'e'+decimals)+'e-'+decimals); + // Shift + value = value.toString().split('e'); + value = Math.floor(+(value[0] + 'e' + (value[1] ? (+value[1] + decimals) : decimals))); + // Shift back + value = value.toString().split('e'); + return Number((value[0] + 'e' + (value[1] ? (+value[1] - decimals) : -decimals))); }; tools.prototype.rangeToArray = function(range) { From bb713673f4fe437f3c812404b174c5037cbfa46a Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Thu, 15 Jan 2015 20:35:14 +0100 Subject: [PATCH 40/57] v0.9.5 Fixed API Call Queue - Fixed the API Call Queue to make sure it throttles request to 1 request per 2 seconds - Removed backtester.js it is now called via "app.js -b" --- LICENSE | 211 +++++++++++++++++++++++++++---- README.md | 60 +-------- app.js | 208 +++++++------------------------ apps/backtester.js | 52 ++++++++ apps/trader.js | 164 ++++++++++++++++++++++++ config.sample.js | 27 ++-- exchanges/bitstamp.js | 27 ++-- exchanges/btce.js | 27 ++-- exchanges/kraken.js | 53 ++++---- exchanges/template.js | 30 +++-- indicators/MACD.js | 6 + indicators/PPO.js | 6 + indicators/PSAR.js | 6 + indicators/template.js | 9 +- package.json | 11 +- services/exchangeapi.js | 20 ++- services/pricemonitor.js | 115 ----------------- services/simulator.js | 249 +++++++++++++++++++++++++++++++++++++ services/tradingadvisor.js | 35 ++++-- services/tradingagent.js | 8 +- tests/apiQueue.js | 59 +++++++++ 21 files changed, 941 insertions(+), 442 deletions(-) create mode 100644 apps/backtester.js create mode 100644 apps/trader.js delete mode 100644 services/pricemonitor.js create mode 100644 services/simulator.js create mode 100644 tests/apiQueue.js diff --git a/LICENSE b/LICENSE index fe027c4..dd35269 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,190 @@ -The MIT License (MIT) - -Copyright (c) 2014 Ruben Callewaert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2015 Ruben Callewaert + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 21af91f..fc2aa73 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ When running the bot initially make sure to run with real trading disabled: Choose an exchange you want to trade on in the exchangeSettings: exchange: '', - // Options: (bitstamp, kraken) + // Options: (bitstamp, kraken, btce) Pay close attention to the currencyPair settings: @@ -75,7 +75,7 @@ If you have a local MongoDB install your connection string would probably look s config.mongoConnectionString = 'localhost/nameofyourchoosing'; -By default you will see a lot of debugging information, once you are sure that your bot is retrieving data change the debug setting to false: +By default you will see a lot of debugging information in the log file located in the logs folder, once you are sure that your bot is retrieving data change the debug setting to false: config.debug = false; @@ -91,57 +91,7 @@ Please read up on the following articles to help you choose the settings that be - [Candlesticks](http://en.wikipedia.org/wiki/Candlestick_chart) - [MACD](http://en.wikipedia.org/wiki/MACD) -As of version 0.7 BitBot now uses indicators as small plugins. You can create your own indicator by using the following template: - - //-------------------- REMOVE THIS BLOCK - console.log('If you want this code to do anything, remove this code block!'); - process.exit(); - //-------------------- REMOVE THIS BLOCK - - var _ = require('underscore'); - var tools = require('../util/tools.js'); - - var indicator = function(options) { - - this.options = options; - this.position = {}; - - _.bindAll(this, 'calculate', 'setPosition'); - - // indicatorOptions - // options: {The options required for your indicator to work} - - }; - - //-------------------------------------------------------------------------------HelperFunctions - - // Insert your helper functions here if needed - - //-------------------------------------------------------------------------------HelperFunctions - - indicator.prototype.calculate = function(cs) { - - // This function receives a candlestick from the trading advisor, this is the layout of a candlestick: - // {'period':timestamp, 'open':open price, 'high':high price, 'low':low price, 'close':close price, 'volume':volume, 'vwap':volume weighted average price} - - // Insert your calculation logic here - - // When done you should always return either 'buy', 'sell' or 'hold' And either the indicatorResult or the value null - return {advice: advice, indicatorResult: indicatorResult}; - - }; - - indicator.prototype.setPosition = function(pos) { - - // This function is required and shouldn't be changed unless you know what you are doing. - // Provides the indicator with information about the current position. - // {pos: position, price: price} - - this.position = pos; - - }; - - module.exports = indicator; +As of version 0.7 BitBot now uses indicators as small plugins. You can create your own indicator by using the template in the indicators folder. For examples on how to use this template, go have a look at one of the existing indicators in the indicators folder. @@ -168,13 +118,13 @@ To run BitBot, execute the following command in the folder where you installed B If you would like to simulate trading on your collected data, execute: - node backtester.js + node app.js -b Remember the backtester simulates your trading strategy on the data you collected. So the longer you keep the bot (app.js) running, the more significant the results of the backtester. ## Profitability -The provided trading algorithms are well known and documented on the internet (MADC, PPO). I do not guarantee you will make any profit when using this bot... +The provided trading algorithms are well known and documented on the internet (MACD, PPO, PSAR). I do not guarantee you will make any profit when using this bot... For better results, consider writing your own algorithm and share it with the community in a pull request :-). ## FAQ diff --git a/app.js b/app.js index ad9254b..89ca822 100644 --- a/app.js +++ b/app.js @@ -1,17 +1,6 @@ var _ = require('underscore'); var loggingservice = require('./services/loggingservice.js'); -var storageservice = require('./services/storage.js'); -var exchangeapiservice = require('./services/exchangeapi.js'); -var dataretriever = require('./services/dataretriever.js'); -var dataprocessor = require('./services/dataprocessor.js'); -var candleaggregator = require('./services/candleaggregator'); -var tradingadvisor = require('./services/tradingadvisor.js'); -var tradingagent = require('./services/tradingagent.js'); -var pushservice = require('./services/pushservice.js'); -var ordermonitor = require('./services/ordermonitor.js'); -var profitreporter = require('./services/profitreporter.js'); -var pricemonitor = require('./services/pricemonitor'); //------------------------------Config var config = require('./config.js'); @@ -19,178 +8,71 @@ var config = require('./config.js'); //------------------------------IntializeModules var logger = new loggingservice('app', config.debug); -var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); -var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); -var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); -var processor = new dataprocessor(storage, logger); -var aggregator = new candleaggregator(config.indicatorSettings.candleStickSizeMinutes, storage, logger); -var advisor = new tradingadvisor(config.indicatorSettings, false, storage, logger); -var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings, storage, exchangeapi, logger); -var pusher = new pushservice(config.pushOver, logger); -var monitor = new ordermonitor(exchangeapi, logger); -var reporter = new profitreporter(config.exchangeSettings.currencyPair, storage, exchangeapi, logger); -var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.indicatorSettings.candleStickSizeMinutes, storage, logger); //------------------------------IntializeModules -retriever.on('update', function(ticks){ +//------------------------------AnnounceStart +logger.log('----------------------------------------------------'); +logger.log('Starting BitBot v0.9.5'); +logger.log('Real Trading Enabled = ' + config.tradingEnabled); +logger.log('Working Dir = ' + process.cwd()); +logger.log('----------------------------------------------------'); +//------------------------------AnnounceStart - processor.updateCandleDB(ticks); +var app = function() { -}); + _.bindAll(this, 'appListener', 'launchTrader', 'launchBacktester', 'start'); -processor.on('initialDBWrite', function(){ - - reporter.start(config.resetInitialBalances); - - advisor.start(); +}; -}); +app.prototype.appListener = function() { -processor.on('update', function(cs){ + this.app.on('done', function() { + logger.log('App closed.'); + }.bind(this)); - if(config.stoplossSettings.enabled) { +}; - pricemon.check(cs.close); +app.prototype.launchTrader = function() { - } + this.createCluster(); + logger.log('----------------------------------------------------'); + logger.log('Launching trader module.'); + logger.log('----------------------------------------------------'); + this.app = require('./apps/trader.js'); + this.appListener(); + this.app.start(); - aggregator.update(); +} -}); +app.prototype.launchBacktester = function() { -aggregator.on('update', function(cs){ + this.createCluster(); + logger.log('----------------------------------------------------'); + logger.log('Launching backtester module.'); + logger.log('----------------------------------------------------'); + this.app = require('./apps/backtester.js'); + this.appListener(); + this.app.start(); - if(config.stoplossSettings.enabled) { +} - pricemon.update(cs, function(err) { +app.prototype.start = function() { - advisor.update(cs, false); - - }); + var argument = process.argv[2]; + if(!argument) { + this.launchTrader(); + } else { + if(argument === '-b') { + this.launchBacktester(); } else { - - advisor.update(cs, false); - + logger.log('Invalid argument, supported options:'); + logger.log('-b: Launch Backtester'); } - -}); - -advisor.on('advice', function(advice){ - - if(advice === 'buy') { - - agent.order(advice); - - } else if(advice === 'sell') { - - agent.order(advice); - - } - -}); - -agent.on('realOrder',function(orderDetails){ - - if(config.pushOver.enabled) { - pusher.send('BitBot - Order Placed!', 'Placed ' + orderDetails.orderType + ' order: (' + orderDetails.amount + '@' + orderDetails.price + ')', 'magic', 1); - } - - monitor.add(orderDetails, config.orderKeepAliveMinutes); - -}); - -agent.on('simulatedOrder',function(orderDetails){ - - if(config.pushOver.enabled) { - pusher.send('BitBot - Order Simulated!', 'Simulated ' + orderDetails.orderType + ' order: (' + orderDetails.amount + '@' + orderDetails.price + ')', 'magic', 1); - } - - monitor.add(orderDetails, config.orderKeepAliveMinutes); - -}); - -monitor.on('filled', function(order) { - - if(order.orderDetails.orderType === 'buy') { - - pricemon.setPosition('bought', order.orderDetails.price); - advisor.setPosition({pos: 'bought', price: order.orderDetails.price}); - - } else if(order.orderDetails.orderType === 'sell') { - - pricemon.setPosition('sold', order.orderDetails.price); - advisor.setPosition({pos: 'sold', price: order.orderDetails.price}); - - } - - reporter.updateBalance(true); - -}); - -monitor.on('cancelled', function(order, retry) { - - reporter.updateBalance(false); - - if(retry) { - - cancelledOrderRetryTimeout = setTimeout(function(){ - - agent.order(order.orderDetails.orderType); - - }, 1000 * 5); - - } - -}); - -pricemon.on('advice', function(advice) { - - if(advice === 'buy') { - - agent.order(advice); - - } else if(advice === 'sell') { - - agent.order(advice); - - } - -}); - -reporter.on('report', function(report){ - - if(config.pushOver.enabled) { - pusher.send('BitBot - Profit Report!', report, 'magic', 1); - } - -}); - -var start = function() { - - //------------------------------AnnounceStart - logger.log('------------------------------------------'); - logger.log('Starting BitBot v0.9.4'); - logger.log('Real Trading Enabled = ' + config.tradingEnabled); - logger.log('Working Dir = ' + process.cwd()); - logger.log('------------------------------------------'); - //------------------------------AnnounceStart - - retriever.start(); + } }; -var stop = function(cb) { - - retriever.stop(); - - clearTimeout(cancelledOrderRetryTimeout); - - monitor.resolvePreviousOrder(function(){ - logger.log('BitBot stopped succesfully!'); - cb(); - }); - -}; +var application = new app(); -start(); +application.start(); diff --git a/apps/backtester.js b/apps/backtester.js new file mode 100644 index 0000000..4cb2207 --- /dev/null +++ b/apps/backtester.js @@ -0,0 +1,52 @@ +var _ = require('underscore'); +var tools = require('../util/tools.js'); +var async = require('async'); + +var loggingservice = require('../services/loggingservice.js'); +var storageservice = require('../services/storage.js'); +var exchangeapiservice = require('../services/exchangeapi.js'); +var tradingadvisor = require('../services/tradingadvisor.js'); +var simulatorservice = require('../services/simulator.js'); + +//------------------------------Config +var config = require('../config.js'); +//------------------------------Config + +//------------------------------IntializeModules +var logger = new loggingservice('backtester', config.debug); +var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); +var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); +var advisor = new tradingadvisor(config.indicatorSettings, true, storage, logger); +var simulator = new simulatorservice(config.exchangeSettings, config.backTesterSettings, config.indicatorSettings, advisor, logger); +//------------------------------IntializeModules + +var backtester = function() { + + _.bindAll(this, 'start'); + +}; + +//---EventEmitter Setup +var Util = require('util'); +var EventEmitter = require('events').EventEmitter; +Util.inherits(backtester, EventEmitter); +//---EventEmitter Setup + +backtester.prototype.start = function() { + async.series( + { + balance: function(cb) {exchangeapi.getBalance(true, cb);}, + aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(config.indicatorSettings.candleStickSizeMinutes, cb);} + }, function(err, result) { + if(result.aggregatedCandleSticks.length > 0) { + var result = simulator.calculate(result.aggregatedCandleSticks, result.balance.fee); + simulator.report(); + this.emit('done'); + } + }.bind(this) + ); +}; + +var backtesterApp = new backtester(); + +module.exports = backtesterApp; diff --git a/apps/trader.js b/apps/trader.js new file mode 100644 index 0000000..1e36018 --- /dev/null +++ b/apps/trader.js @@ -0,0 +1,164 @@ +var _ = require('underscore'); + +var loggingservice = require('../services/loggingservice.js'); +var storageservice = require('../services/storage.js'); +var exchangeapiservice = require('../services/exchangeapi.js'); +var dataretriever = require('../services/dataretriever.js'); +var dataprocessor = require('../services/dataprocessor.js'); +var candleaggregator = require('../services/candleaggregator'); +var tradingadvisor = require('../services/tradingadvisor.js'); +var tradingagent = require('../services/tradingagent.js'); +var pushservice = require('../services/pushservice.js'); +var ordermonitor = require('../services/ordermonitor.js'); +var profitreporter = require('../services/profitreporter.js'); + +//------------------------------Config +var config = require('../config.js'); +//------------------------------Config + +//------------------------------IntializeModules +var logger = new loggingservice('app', config.debug); +var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); +var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); +var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); +var processor = new dataprocessor(storage, logger); +var aggregator = new candleaggregator(config.indicatorSettings.candleStickSizeMinutes, storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, false, storage, logger); +var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings, storage, exchangeapi, logger); +var pusher = new pushservice(config.pushOver, logger); +var monitor = new ordermonitor(exchangeapi, logger); +var reporter = new profitreporter(config.exchangeSettings.currencyPair, storage, exchangeapi, logger); +//------------------------------IntializeModules + +var trader = function() { + + retriever.on('update', function(ticks){ + + processor.updateCandleDB(ticks); + + }); + + processor.on('initialDBWrite', function(){ + + reporter.start(config.resetInitialBalances); + + advisor.start(); + + }); + + processor.on('update', function(cs){ + + aggregator.update(); + + }); + + aggregator.on('update', function(cs){ + + var advice = advisor.update(cs, false); + + if(advice === 'buy') { + + agent.order(advice); + + } else if(advice === 'sell') { + + agent.order(advice); + + } + + }); + + agent.on('realOrder',function(orderDetails){ + + if(config.pushOver.enabled) { + pusher.send('BitBot - Order Placed!', 'Placed ' + orderDetails.orderType + ' order: (' + orderDetails.amount + '@' + orderDetails.price + ')', 'magic', 1); + } + + monitor.add(orderDetails, config.orderKeepAliveMinutes); + + }); + + agent.on('simulatedOrder',function(orderDetails){ + + if(config.pushOver.enabled) { + pusher.send('BitBot - Order Simulated!', 'Simulated ' + orderDetails.orderType + ' order: (' + orderDetails.amount + '@' + orderDetails.price + ')', 'magic', 1); + } + + monitor.add(orderDetails, config.orderKeepAliveMinutes); + + }); + + monitor.on('filled', function(order) { + + if(order.orderDetails.orderType === 'buy') { + + advisor.setPosition({pos: 'bought', price: order.orderDetails.price}); + + } else if(order.orderDetails.orderType === 'sell') { + + advisor.setPosition({pos: 'sold', price: order.orderDetails.price}); + + } + + reporter.updateBalance(true); + + }); + + monitor.on('cancelled', function(order, retry) { + + reporter.updateBalance(false); + + if(retry) { + + cancelledOrderRetryTimeout = setTimeout(function(){ + + agent.order(order.orderDetails.orderType); + + }, 1000 * 5); + + } + + }); + + reporter.on('report', function(report){ + + if(config.pushOver.enabled) { + pusher.send('BitBot - Profit Report!', report, 'magic', 1); + } + + }); + + _.bindAll(this, 'start', 'stop'); + +} + +//---EventEmitter Setup +var Util = require('util'); +var EventEmitter = require('events').EventEmitter; +Util.inherits(trader, EventEmitter); +//---EventEmitter Setup + +trader.prototype.start = function() { + + retriever.start(); + +}; + +trader.prototype.stop = function(cb) { + + retriever.stop(); + + clearTimeout(cancelledOrderRetryTimeout); + + monitor.resolvePreviousOrder(function() { + logger.log('BitBot stopped succesfully!'); + cb(); + }.bind(this)); + + this.emit('done'); + +}; + +var traderApp = new trader(); + +module.exports = traderApp; diff --git a/config.sample.js b/config.sample.js index fed4bf3..e0728db 100644 --- a/config.sample.js +++ b/config.sample.js @@ -17,10 +17,11 @@ config.exchangeSettings = { // For BTC-E look up the currency pairs in their API: https://btc-e.com/api/3/info // BTC-E Example: {pair: 'BTC_USD', asset: 'BTC', currency: 'USD'} tradingReserveAsset: 0, - // Enter an amount of "asset" you would like to freeze (not trade) + // Enter an amount of "asset" you would like to freeze (not trade). tradingReserveCurrency: 0, - // Enter an amount of "currency" you would like to freeze (not trade) + // Enter an amount of "currency" you would like to freeze (not trade). slippagePercentage: 0.1 + // Percentage to sell below and buy above the market. }; //------------------------------exchangeSettings @@ -34,23 +35,23 @@ config.apiSettings = { //------------------------------dbSettings config.mongoConnectionString = 'localhost/bitbot'; -// The connection string for your MongoDB Installation +// The connection string for your MongoDB Installation. // Example: config.mongoConnectionString = 'username:password@example.com/mydb'; config.resetInitialBalances = false; -// Set this to true if you want to reset the initialBalance stored by the bot +// Set this to true if you want to reset the initialBalance stored by the bot. //------------------------------dbSettings //------------------------------downloaderSettings config.downloaderRefreshSeconds = 10; -// Best to keep this default setting unless you know what you are doing +// Best to keep this default setting unless you know what you are doing. //------------------------------downloaderSettings //------------------------------IndicatorSettings config.indicatorSettings = { indicator: 'MACD', - // Choices: Any indicator from the indicators folder + // Choices: Any indicator from the indicators folder. options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0}, - // Options needed for your indicator (Look them up in the indicator's file) + // Options needed for your indicator (Look them up in the indicator's file). candleStickSizeMinutes: 5 }; //------------------------------IndicatorSettings @@ -59,25 +60,17 @@ config.indicatorSettings = { config.orderKeepAliveMinutes = config.indicatorSettings.candleStickSizeMinutes / 10; //------------------------------orderSettings -//------------------------------stopLossSettings -config.stoplossSettings = { - enabled: false, - percentageBought: 1, - percentageSold: 1 -}; -//------------------------------stopLossSettings - //------------------------------PushOver config.pushOver = { enabled: false, pushUserId: '', pushAppToken: '' }; -// You can receive push notifications using pushover if you fill in these settings (https://pushover.net/). +// Push notifications via pushover (https://pushover.net/). //------------------------------PushOver //------------------------------BackTesting -config.backTesting = { +config.backTesterSettings = { initialAssetBalance: 0, initialCurrencyBalance: 10000 }; diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 15af9ea..641d6e3 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -11,8 +11,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.q = async.queue(function (task, callback) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); - task.func(); - setTimeout(callback,2000); + task.func(function() { setTimeout(callback, 2000); }); }.bind(this), 1); this.logger = logger; @@ -80,12 +79,14 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var pair = this.currencyPair.pair; var handler = function(err, response) { + finish(); + if(!err) { var trades = _.map(response, function(t) { @@ -118,7 +119,7 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var asset = this.currencyPair.asset; var currency = this.currencyPair.currency; @@ -127,6 +128,8 @@ exchange.prototype.getBalance = function(retry, cb) { var handler = function(err, result) { + finish(); + if(!err) { cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); @@ -151,12 +154,14 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function () { + var wrapper = function (finish) { var pair = this.currencyPair.pair; var handler = function(err, result) { + finish(); + if(!err) { var bids = _.map(result.bids, function(bid) { @@ -189,12 +194,14 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var pair = this.currencyPair.pair; var handler = function(err, result) { + finish(); + if(!err) { if(!result.error) { @@ -239,10 +246,12 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, result) { + finish(); + if(!err) { var open = _.find(result, function(o) { @@ -285,10 +294,12 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { if(!filled && !err) { - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, result) { + finish(); + if(!err) { if(!result.error) { diff --git a/exchanges/btce.js b/exchanges/btce.js index 2c4bf10..a8c305a 100644 --- a/exchanges/btce.js +++ b/exchanges/btce.js @@ -11,8 +11,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.q = async.queue(function (task, callback) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); - task.func(); - setTimeout(callback,2000); + task.func(function() { setTimeout(callback, 2000); }); }.bind(this), 1); this.logger = logger; @@ -80,12 +79,14 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var pair = this.currencyPair.pair.toLowerCase(); var handler = function(err, response) { + finish(); + if(!err) { var trades = _.map(response.reverse(), function(entry) { @@ -116,13 +117,15 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var asset = this.currencyPair.asset.toLowerCase(); var currency = this.currencyPair.currency.toLowerCase(); var handler = function(err, response) { + finish(); + if(!err) { cb(null, {assetAvailable: response.funds[asset], currencyAvailable: response.funds[currency], fee: 0.2}); @@ -147,12 +150,14 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var pair = this.currencyPair.pair.toLowerCase(); var handler = function(err, response) { + finish(); + if(!err) { var bids = _.map(response.bids, function(bid) { @@ -185,12 +190,14 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var pair = this.currencyPair.pair.toLowerCase(); var handler = function(err, response) { + finish(); + if(!err) { var status = 'open'; @@ -235,10 +242,12 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + if(!err) { if(response[order]) { @@ -275,10 +284,12 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { if(!filled && !err) { - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + if(!err) { if(response.order_id === order) { diff --git a/exchanges/kraken.js b/exchanges/kraken.js index 39e352c..f9cc9ef 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -11,8 +11,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.q = async.queue(function (task, callback) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); - task.func(); - setTimeout(callback,2000); + task.func(function() { setTimeout(callback, 2000); }); }.bind(this), 1); this.logger = logger; @@ -89,12 +88,14 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finished) { var pair = this.currencyPair.pair; var handler = function(err, data) { + finished(); + if(!err) { var values = _.find(data.result, function(value, key) { @@ -131,7 +132,7 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finished) { var asset = this.currencyPair.asset; var currency = this.currencyPair.currency; @@ -158,34 +159,32 @@ exchange.prototype.getBalance = function(retry, cb) { currencyValue = 0; } - var secondWrapper = function() { - - var secondHandler = function(err, data) { + var secondHandler = function(err, data) { - if(!err) { + finished(); - var fee = parseFloat(_.find(data.result.fees, function(value, key) { - return key === pair; - }).fee); + if(!err) { - cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + var fee = parseFloat(_.find(data.result.fees, function(value, key) { + return key === pair; + }).fee); - } else { + cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); - cb(err, null); - - } + } else { - }.bind(this); + cb(err, null); - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, retry, 'getBalance', secondHandler)); + } }.bind(this); - this.q.push({name: 'TradeVolume', func: secondWrapper}); + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, retry, 'getBalance', secondHandler)); } else { + finished(); + cb(err, null); } @@ -204,12 +203,14 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function () { + var wrapper = function (finished) { var pair = this.currencyPair.pair; var handler = function(err, data) { + finished(); + if(!err) { var orderbook = _.find(data.result, function(value, key) { @@ -248,12 +249,14 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finished) { var pair = this.currencyPair.pair; var handler = function(err, data) { + finished(); + if(!err) { cb(null, {txid: data.result.txid[0], status: 'open'}); @@ -290,10 +293,12 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finished) { var handler = function(err, data) { + finished(); + if(!err) { var open = _.find(data.result.open, function(value, key) { @@ -336,10 +341,12 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { if(!filled && !err) { - var wrapper = function() { + var wrapper = function(finished) { var handler = function(err, data) { + finished(); + if(!err) { if(data.result.count > 0) { diff --git a/exchanges/template.js b/exchanges/template.js index e01f4bc..5afa149 100644 --- a/exchanges/template.js +++ b/exchanges/template.js @@ -1,5 +1,6 @@ //-------------------- REMOVE THIS BLOCK -console.log('If you want this code to do anything, remove this code block!'); +var err = new Error('If you want this code to do anything, remove this code block!'); +this.logger.error(err.stack); process.exit(); //-------------------- REMOVE THIS BLOCK @@ -15,8 +16,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.q = async.queue(function (task, callback) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); - task.func(); - setTimeout(callback,2000); + task.func(function() { setTimeout(callback, 2000); }); }.bind(this), 1); this.logger = logger; @@ -84,10 +84,12 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + cb(null, [{date: timestamp, price: number, amount: number}]); }; @@ -105,10 +107,12 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + cb(null, {currencyAvailable: number, assetAvailable: number, fee: number}); }; @@ -126,10 +130,12 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + cb(null, {bids: [{assetAmount: number, currencyPrice: number}], asks: [{assetAmount: number, currencyPrice: number}]}); }; @@ -147,10 +153,12 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + cb(null, {txid: transaction_id, status: 'open'}); }; @@ -168,10 +176,12 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + cb(null, boolean); }; @@ -189,10 +199,12 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; - var wrapper = function() { + var wrapper = function(finish) { var handler = function(err, response) { + finish(); + cb(null, boolean); }; diff --git a/indicators/MACD.js b/indicators/MACD.js index 8e4d291..ef11db7 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -12,6 +12,12 @@ var indicator = function(options) { _.bindAll(this, 'calculate', 'setPosition'); + if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyTreshold' in options || !'sellTreshold' in options) { + var err = new Error('Invalid options for MACD indicator, exiting.'); + this.logger.error(err.stack); + process.exit(); + } + // indicatorOptions // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} diff --git a/indicators/PPO.js b/indicators/PPO.js index a34579b..c8dd050 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -12,6 +12,12 @@ var indicator = function(options) { _.bindAll(this, 'calculate', 'setPosition'); + if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyTreshold' in options || !'sellTreshold' in options) { + var err = new Error('Invalid options for PPO indicator, exiting.'); + this.logger.error(err.stack); + process.exit(); + } + // indicatorOptions // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} diff --git a/indicators/PSAR.js b/indicators/PSAR.js index e631ebd..de08b56 100644 --- a/indicators/PSAR.js +++ b/indicators/PSAR.js @@ -10,6 +10,12 @@ var indicator = function(options) { _.bindAll(this, 'calculate', 'setPosition'); + if(!'AFIncrement' in options || !'maximumAF' in options) { + var err = new Error('Invalid options for PSAR indicator, exiting.'); + this.logger.error(err.stack); + process.exit(); + } + // indicatorOptions // options: {AFIncrement: number, maximumAF: number} diff --git a/indicators/template.js b/indicators/template.js index aadc6cc..006c224 100644 --- a/indicators/template.js +++ b/indicators/template.js @@ -1,5 +1,6 @@ //-------------------- REMOVE THIS BLOCK -console.log('If you want this code to do anything, remove this code block!'); +var err = new Error('If you want this code to do anything, remove this code block!'); +this.logger.error(err.stack); process.exit(); //-------------------- REMOVE THIS BLOCK @@ -13,6 +14,12 @@ var indicator = function(options) { _.bindAll(this, 'calculate', 'setPosition'); + if(!'option' in options) { + var err = new Error('Invalid options for indicator, exiting.'); + this.logger.error(err.stack); + process.exit(); + } + // indicatorOptions // options: {The options required for your indicator to work} diff --git a/package.json b/package.json index 71a1258..0f72488 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,20 @@ { "name": "BitBot", - "version": "0.9.4", + "version": "0.9.5", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { - "async": "0.9.0", + "async": "0.9.2", "flat": "1.3.0", "bitstamp-api": "0.1.9", "mime": "1.2.11", "moment": "2.4.0", - "mongojs": "0.15.1", + "mongojs": "0.18.0", "pushover-notifications": "0.1.5", - "underscore": "1.6.0", - "ws": "0.4.31", + "underscore": "1.7.0", "kraken-exchange-api": "0.1.0", "btc-e": "1.0.2", - "winston": "0.8.0" + "winston": "0.8.3" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/services/exchangeapi.js b/services/exchangeapi.js index d11c54c..80c89a3 100644 --- a/services/exchangeapi.js +++ b/services/exchangeapi.js @@ -7,13 +7,25 @@ var api = function(exchangeSettings, apiSettings, logger) { this.currencyPair = exchangeSettings.currencyPair; this.logger = logger; - if(fs.existsSync('./exchanges/' + this.exchange + '.js')) { - var Exchange = require('../exchanges/' + this.exchange + '.js'); - this.selectedExchange = new Exchange(this.currencyPair, apiSettings[this.exchange], logger); - } else { + try { + + this.exchanges = {}; + + fs.readdirSync('./exchanges/').forEach(function(file) { + if(file != 'template.js' && file.indexOf('.') > 0 && file.indexOf('.js') > 0) { + var exchange = require('../exchanges/' + file); + this.exchanges[file.replace('.js', '')] = exchange; + } + }.bind(this)); + + this.selectedExchange = new this.exchanges[this.exchange](this.currencyPair, apiSettings[this.exchange], this.logger); + + } catch(err) { + var err = new Error('Wrong exchange chosen. This exchange doesn\'t exist.'); this.logger.error(err.stack); process.exit(); + } _.bindAll(this, 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); diff --git a/services/pricemonitor.js b/services/pricemonitor.js deleted file mode 100644 index 0737bec..0000000 --- a/services/pricemonitor.js +++ /dev/null @@ -1,115 +0,0 @@ -var _ = require('underscore'); -var tools = require('../util/tools.js'); - -var monitor = function(slPercentageB, slPercentageS, candleStickSizeMinutes, storage, logger) { - - this.percentageBought = slPercentageB; - this.percentageSold = slPercentageS; - this.candleStickSizeMinutes = candleStickSizeMinutes; - this.storage = storage; - this.logger = logger; - - this.position = 'none'; - - _.bindAll(this, 'check', 'setPosition', 'update'); - -}; - -//---EventEmitter Setup -var Util = require('util'); -var EventEmitter = require('events').EventEmitter; -Util.inherits(monitor, EventEmitter); -//---EventEmitter Setup - -monitor.prototype.check = function(price) { - - if(this.position === 'bought') { - - if(price <= this.checkPriceBought) { - this.logger.log('Stop Loss triggered (Long Entry: ' + this.posPrice + ' Exit: ' + price + ')'); - this.position = 'none'; - this.posPrice = 0; - this.emit('advice', 'sell'); - } - - } else if(this.position === 'sold') { - - if(price >= this.checkPriceSold) { - this.logger.log('Stop Loss triggered (Short Entry: ' + this.posPrice + ' Exit: ' + price + ')'); - this.position = 'none'; - this.posPrice = 0; - this.emit('advice', 'buy'); - } - - } else { - - this.emit('advice', 'hold'); - - } - -}; - -monitor.prototype.setPosition = function(pos, price) { - - if(pos === 'bought') { - - this.position = 'bought'; - this.posPrice = price; - this.checkPriceBought = this.posPrice * (1 - (this.percentageBought / 100)); - - } else if(pos === 'sold') { - - this.position = 'sold'; - this.posPrice = price; - this.checkPriceSold = this.posPrice * (1 + (this.percentageSold / 100)); - - } - -}; - -monitor.prototype.update = function(cs, callback) { - - this.storage.getLastNCompleteAggregatedCandleSticks(10, this.candleStickSizeMinutes, function(err, completeCandleSticks) { - - var averageSize = 0; - - if(completeCandleSticks.length > 0) { - averageSize = tools.floor(_.reduce(completeCandleSticks, function(memo, entry){ return memo + Math.abs(entry.close - entry.open); }, 0) / 10, 2); - } - - var diff = cs.close - cs.open; - var size = Math.abs(tools.round(cs.close - cs.open, 2)); - - var change = tools.round(size / 2, 2); - - var newSl; - - if(size >= averageSize * 2) { - - if(this.position === 'bought' && diff > 0) { - - newSl = tools.round(this.checkPriceBought + change, 2); - - this.logger.log('Stop loss increased! Old: ' + this.checkPriceBought + ' New: ' + newSl); - - this.checkPriceBought = newSl; - - } else if(this.position === 'sold' && diff < 0) { - - newSl = tools.round(this.checkPriceSold - change, 2); - - this.logger.log('Stop loss decreased! Old: ' + this.checkPriceSold + ' New: ' + newSl); - - this.checkPriceSold = newSl; - - } - - } - - callback(null); - - }.bind(this)); - -}; - -module.exports = monitor; diff --git a/services/simulator.js b/services/simulator.js new file mode 100644 index 0000000..e2ed440 --- /dev/null +++ b/services/simulator.js @@ -0,0 +1,249 @@ +var _ = require('underscore'); +var tools = require('../util/tools.js'); +var moment = require('moment'); +var async = require('async'); + +var simulator = function(exchangeSettings, backTesterSettings, indicatorSettings, advisor, logger){ + + this.logger = logger; + + this.options = {}; + + this.options.exchange = exchangeSettings.exchange; + this.options.asset = exchangeSettings.currencyPair.asset; + this.options.currency = exchangeSettings.currencyPair.currency; + this.options.slippagePercentage = exchangeSettings.slippagePercentage; + this.options.initialAssetBalance = backTesterSettings.initialAssetBalance; + this.options.initialCurrencyBalance = backTesterSettings.initialCurrencyBalance; + this.options.candleStickSizeMinutes = indicatorSettings.candleStickSizeMinutes; + + this.advisor = advisor; + + // Set Variables + this.options.currencyBalance = this.options.initialCurrencyBalance; + this.options.assetBalance = this.options.initialAssetBalance; + this.options.initialBalanceSumInAsset = 0; + this.options.initialBalanceSumInCurrency = 0; + this.options.totalBalanceInCurrency = 0; + this.options.totalBalanceInAsset = 0; + this.options.profit = 0; + this.options.profitPercentage = 0; + this.options.bhProfit = 0; + this.options.bhProfitPercentage = 0; + this.options.transactionFee = 0; + this.options.totalTradedVolume = 0; + this.options.highestCurrencyValue = this.options.initialCurrencyBalance; + this.options.lowestCurrencyValue = this.options.initialCurrencyBalance; + this.options.totalFeeCosts = 0; + this.options.totalFeeCostsPercentage = 0; + this.options.latestCandlePeriod; + this.options.lastClose = 0; + this.options.lastClosePlusSlippage = 0; + this.options.lastCloseMinusSlippage = 0; + this.options.csPeriod = 0; + this.options.entryCurrency = 0; + this.options.exitCurrency = 0; + this.options.winners = 0; + this.options.losers = 0; + this.options.bigWinner = 0; + this.options.bigLoser = 0; + this.options.totalGain = 0; + this.options.totalLoss = 0; + this.options.averageGain = 0; + this.options.averageLoss = 0; + this.options.transactions = []; + // Set Variables + + _.bindAll(this, 'calculate', 'postProcess', 'createOrder', 'report'); + +}; + +simulator.prototype.calculate = function(csArray, transactionFee) { + + // Set Variables + this.options.currencyBalance = this.options.initialCurrencyBalance; + this.options.assetBalance = this.options.initialAssetBalance; + this.options.totalFeeCosts = 0; + this.options.transactionFee = 0; + this.options.totalTradedVolume = 0; + this.options.highestCurrencyValue = this.options.initialCurrencyBalance; + this.options.lowestCurrencyValue = this.options.initialCurrencyBalance; + this.options.winners = 0; + this.options.losers = 0; + this.options.bigWinner = 0; + this.options.bigLoser = 0; + this.options.totalGain = 0; + this.options.totalLoss = 0; + this.options.transactions = []; + // Set Variables + + this.options.transactionFee = transactionFee; + + this.options.initialBalanceSumInAsset = this.options.assetBalance + tools.round(this.options.currencyBalance / _.first(csArray).close, 8); + this.options.initialBalanceSumInCurrency = this.options.currencyBalance + tools.round(this.options.assetBalance * _.first(csArray).close, 8); + + this.options.firstCs = _.first(csArray); + this.options.lastCs = _.last(csArray); + + _.forEach(csArray, function(cs) { + + this.options.latestCandlePeriod = cs.period; + + this.options.lastClose = cs.close; + + this.logger.debug('Backtest: Created a new ' + this.options.candleStickSizeMinutes + ' minute candlestick!'); + this.logger.debug(JSON.stringify(cs)); + + var advice = this.advisor.update(cs); + + if(advice !== 'hold') { + this.createOrder(advice); + } + + }.bind(this)); + + this.postProcess(); + + return this.options; + +}; + +simulator.prototype.postProcess = function() { + + this.options.totalBalanceInCurrency = tools.round(this.options.currencyBalance + (this.options.assetBalance * this.options.lastClose), 8); + this.options.totalBalanceInAsset = tools.round(this.options.assetBalance + (this.options.currencyBalance / this.options.lastClose), 8); + this.options.profit = tools.round(this.options.totalBalanceInCurrency - this.options.initialBalanceSumInCurrency, 8); + this.options.profitPercentage = tools.round(this.options.profit / this.options.initialBalanceSumInCurrency * 100, 8); + this.options.totalFeeCostsPercentage = tools.round(this.options.totalFeeCosts / this.options.initialBalanceSumInCurrency * 100, 8); + this.options.bhProfit = tools.round((this.options.lastCs.close - this.options.firstCs.open) * this.options.initialBalanceSumInAsset, 8); + this.options.bhProfitPercentage = tools.round(this.options.bhProfit / this.options.initialBalanceSumInCurrency * 100, 8); + + if(this.options.totalBalanceInCurrency > this.options.highestCurrencyValue) { + this.options.highestCurrencyValue = this.options.totalBalanceInCurrency; + } + + this.options.startDate = moment(new Date(this.options.firstCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); + this.options.endDate = moment(new Date(this.options.lastCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); + + this.options.openPrice = this.options.firstCs.open; + this.options.closePrice = this.options.lastCs.close; + + this.options.averageGain = tools.round(this.options.totalGain / this.options.winners, 8); + this.options.averageLoss = tools.round(this.options.totalLoss / this.options.losers, 8); + +} + +simulator.prototype.createOrder = function(type) { + + if(type === 'buy' && this.options.currencyBalance > 0) { + + this.options.entryCurrency = this.options.currencyBalance; + + this.options.usableBalance = tools.round(this.options.currencyBalance * (1 - (this.options.transactionFee / 100)), 8); + + this.options.lastClosePlusSlippage = tools.round(this.options.lastClose * (1 + (this.options.slippagePercentage / 100)), 8); + + this.options.totalTradedVolume = tools.round(this.options.totalTradedVolume + this.options.usableBalance, 8); + + this.options.totalFeeCosts = tools.round(this.options.totalFeeCosts + (this.options.currencyBalance * (this.options.transactionFee / 100)), 8); + + this.options.assetBalance = tools.round(this.options.assetBalance + (this.options.usableBalance / this.options.lastClosePlusSlippage), 8); + this.options.currencyBalance = 0; + + this.options.newcurrencyBalance = tools.round(this.options.assetBalance * this.options.lastClosePlusSlippage, 8); + + if(this.options.newcurrencyBalance > this.options.highestCurrencyValue) { + this.options.highestCurrencyValue = this.options.newcurrencyBalance; + } else if(this.options.newcurrencyBalance < this.options.lowestCurrencyValue) { + this.options.lowestCurrencyValue = this.options.newcurrencyBalance; + } + + this.logger.debug(new Date(this.options.latestCandlePeriod * 1000) + ' Placed buy order ' + this.options.assetBalance + ' @ ' + this.options.lastClosePlusSlippage); + this.options.transactions.push(new Date(this.options.latestCandlePeriod * 1000) + ' Placed buy order ' + this.options.assetBalance + ' @ ' + this.options.lastClosePlusSlippage); + + this.advisor.setPosition({pos: 'bought', price: this.options.lastClosePlusSlippage}); + + } else if(type === 'sell' && this.options.assetBalance > 0) { + + this.options.usableBalance = tools.round(this.options.assetBalance * (1 - (this.options.transactionFee / 100)), 8); + + this.options.lastCloseMinusSlippage = tools.round(this.options.lastClose * (1 - (this.options.slippagePercentage / 100)), 8); + + this.options.totalTradedVolume = tools.round(this.options.totalTradedVolume + (this.options.usableBalance * this.options.lastCloseMinusSlippage), 8); + + this.options.totalFeeCosts = tools.round(this.options.totalFeeCosts + (this.options.assetBalance * (this.options.transactionFee / 100) * this.options.lastCloseMinusSlippage), 8); + + this.options.currencyBalance = tools.round(this.options.currencyBalance + (this.options.usableBalance * this.options.lastCloseMinusSlippage), 8); + this.options.assetBalance = 0; + + if(this.options.currencyBalance > this.options.highestCurrencyValue) { + this.options.highestCurrencyValue = this.options.currencyBalance; + } else if(this.options.currencyBalance < this.options.lowestCurrencyValue) { + this.options.lowestCurrencyValue = this.options.currencyBalance; + } + + this.options.exitCurrency = this.options.currencyBalance; + + if(this.options.entryCurrency > 0) { + + this.options.tradeResult = tools.round(this.options.exitCurrency - this.options.entryCurrency, 8); + + if(this.options.exitCurrency > this.options.entryCurrency) { + this.options.winners += 1; + this.options.totalGain = tools.round(this.options.totalGain + this.options.tradeResult, 8); + if(this.options.tradeResult > this.options.bigWinner) {this.options.bigWinner = this.options.tradeResult;} + } else { + this.options.losers += 1; + this.options.totalLoss = tools.round(this.options.totalLoss + this.options.tradeResult, 8); + if(this.options.tradeResult < this.options.bigLoser) {this.options.bigLoser = this.options.tradeResult;} + } + + } + + this.logger.debug(new Date(this.options.latestCandlePeriod * 1000) + ' Placed sell order ' + this.options.usableBalance + ' @ ' + this.options.lastCloseMinusSlippage); + this.options.transactions.push(new Date(this.options.latestCandlePeriod * 1000) + ' Placed sell order ' + this.options.usableBalance + ' @ ' + this.options.lastCloseMinusSlippage); + + this.advisor.setPosition({pos: 'sold', price: this.options.lastCloseMinusSlippage}); + + } else { + + this.logger.debug('Wanted to place a ' + type + ' order @ ' + this.options.lastClose + ', but there are no more funds available to ' + type); + + } + +}; + +simulator.prototype.report = function() { + + this.options.transactions.forEach(function(transaction) { + this.logger.log(transaction); + }.bind(this)) + + this.logger.log('----------Report----------'); + this.logger.log('Exchange: ' + this.options.exchange); + this.logger.log('Transaction Fee: ' + this.options.transactionFee + '%'); + this.logger.log('Initial ' + this.options.asset + ' Balance: ' + this.options.initialAssetBalance); + this.logger.log('Initial ' + this.options.currency + ' Balance: ' + this.options.initialCurrencyBalance); + this.logger.log('Final ' + this.options.asset + ' Balance: ' + this.options.assetBalance); + this.logger.log('Final ' + this.options.currency + ' Balance: ' + this.options.currencyBalance); + this.logger.log('Total Initial Balance in ' + this.options.currency + ': ' + this.options.initialBalanceSumInCurrency); + this.logger.log('Total Initial Balance in ' + this.options.asset + ': ' + this.options.initialBalanceSumInAsset); + this.logger.log('Total Final Balance in ' + this.options.currency + ': ' + this.options.totalBalanceInCurrency); + this.logger.log('Total Final Balance in ' + this.options.asset + ': ' + this.options.totalBalanceInAsset); + this.logger.log('Winning trades : ' + this.options.winners + ' Losing trades: ' + this.options.losers); + this.logger.log('Biggest winner: ' + this.options.bigWinner + ' Biggest loser: ' + this.options.bigLoser); + this.logger.log('Average winner: ' + this.options.averageGain + ' Average loser: ' + this.options.averageLoss); + this.logger.log('Profit: ' + this.options.profit + ' (' + this.options.profitPercentage + '%)'); + this.logger.log('Buy and Hold Profit: ' + this.options.bhProfit + ' (' + this.options.bhProfitPercentage + '%)'); + this.logger.log('Lost on fees: ' + this.options.totalFeeCosts + ' (' + this.options.totalFeeCostsPercentage + '%)'); + this.logger.log('Total traded volue: ' + this.options.totalTradedVolume); + this.logger.log('Highest - Lowest ' + this.options.currency + ' Balance: ' + this.options.highestCurrencyValue + ' - ' + this.options.lowestCurrencyValue); + this.logger.log('Open Price: ' + this.options.openPrice); + this.logger.log('Close Price: ' + this.options.closePrice); + this.logger.log('Start - End Date: ' + this.options.startDate + ' - ' + this.options.endDate); + this.logger.log('Transactions: ' + this.options.transactions.length); + this.logger.log('--------------------------'); + +}; + +module.exports = simulator; diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index edec482..600782d 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -9,24 +9,36 @@ var advisor = function(indicatorSettings, backTesting, storage, logger) { this.storage = storage; this.logger = logger; - if(fs.existsSync('./indicators/' + indicatorSettings.indicator + '.js')) { - var indicator = require('../indicators/' + indicatorSettings.indicator + '.js'); - this.selectedIndicator = new indicator(indicatorSettings.options); - } else { + try { + + this.indicators = {}; + + fs.readdirSync('./indicators/').forEach(function(file) { + if(file != 'template.js' && file.indexOf('.') > 0 && file.indexOf('.js') > 0) { + var indicator = require('../indicators/' + file); + this.indicators[file.replace('.js', '')] = indicator; + } + }.bind(this)); + + this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); + + } catch(err) { + var err = new Error('Wrong indicator chosen. This indicator doesn\'t exist.'); this.logger.error(err.stack); process.exit(); + } - _.bindAll(this, 'start', 'update', 'setPosition'); + _.bindAll(this, 'start', 'update', 'setPosition', 'setIndicator'); }; -//---EventEmitter Setup +/*//---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; Util.inherits(advisor, EventEmitter); -//---EventEmitter Setup +//---EventEmitter Setup*/ advisor.prototype.start = function() { @@ -53,7 +65,8 @@ advisor.prototype.update = function(cs) { } if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { - this.emit('advice', result.advice); + //this.emit('advice', result.advice); + return result.advice; } else { var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); this.logger.error(err.stack); @@ -68,4 +81,10 @@ advisor.prototype.setPosition = function(pos) { }; +advisor.prototype.setIndicator = function(indicatorSettings) { + + this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); + +}; + module.exports = advisor; diff --git a/services/tradingagent.js b/services/tradingagent.js index f8ac00c..38d8e5e 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -57,20 +57,20 @@ agent.prototype.calculateOrder = function(result) { this.orderDetails.assetBalance = parseFloat(result.balance.assetAvailable); this.orderDetails.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.orderDetails.tradingFee = parseFloat(result.balance.fee); + this.orderDetails.transactionFee = parseFloat(result.balance.fee); var orderBook = result.orderBook; var lastClose = result.lastClose; - this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.tradingFee +')'); + this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.transactionFee +')'); if(this.orderDetails.orderType === 'buy') { var lowestAsk = lastClose; var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 8); - var balance = (this.orderDetails.currencyBalance - this.tradingReserveCurrency) * (1 - (this.orderDetails.tradingFee / 100)); + var balance = (this.orderDetails.currencyBalance - this.tradingReserveCurrency) * (1 - (this.orderDetails.transactionFee / 100)); this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); @@ -116,7 +116,7 @@ agent.prototype.placeSimulatedOrder = function() { this.orderDetails.order = 'Simulated'; - this.orderDetails.status = order.status; + this.orderDetails.status = 'open'; this.logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); diff --git a/tests/apiQueue.js b/tests/apiQueue.js new file mode 100644 index 0000000..12af419 --- /dev/null +++ b/tests/apiQueue.js @@ -0,0 +1,59 @@ +//------------------------------Config +var config = require('../config.js'); +//------------------------------Config + +var exchangeapiservice = require('../services/exchangeapi.js'); +var loggingservice = require('../services/loggingservice.js'); +var _ = require('underscore'); +var async = require('async'); + +var logger = new loggingservice('apiQueueTest', config.debug); +var api = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); + +var test = function() { + + var oldTimestamp = new Date().getTime(); + + var call = function(callback) { + + logger.log('Received request for API data.'); + + api.getTrades(false, function(err, result) { + + if(err) { + logger.log('API Returned Error.'); + callback(err); + } else if(result) { + logger.log('Received data from API.'); + callback(null); + } + + }); + + }; + + async.each([call,call], function(func, next) { + + func(next); + + }, function(err) { + + var newTimestamp = new Date().getTime(); + + if(err) { + logger.log('test failed'); + process.exit(); + } else { + if(newTimestamp - oldTimestamp > 2000) { + logger.log('test succeeded'); + } else { + logger.log('test failed'); + } + process.exit(); + } + + }); + +}; + +test(); From 9258491171a3a1611c2ab7fe46984ca71489ee1c Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Thu, 15 Jan 2015 20:53:21 +0100 Subject: [PATCH 41/57] Bugfix --- app.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app.js b/app.js index 89ca822..00ce389 100644 --- a/app.js +++ b/app.js @@ -34,7 +34,6 @@ app.prototype.appListener = function() { app.prototype.launchTrader = function() { - this.createCluster(); logger.log('----------------------------------------------------'); logger.log('Launching trader module.'); logger.log('----------------------------------------------------'); @@ -46,7 +45,6 @@ app.prototype.launchTrader = function() { app.prototype.launchBacktester = function() { - this.createCluster(); logger.log('----------------------------------------------------'); logger.log('Launching backtester module.'); logger.log('----------------------------------------------------'); From 325293a17675c10ae8c7b40c892a8145c5d8dd79 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Sun, 18 Jan 2015 21:40:22 +0100 Subject: [PATCH 42/57] v0.9.6 Stability Improvements - Fixed another bug in the exchangeapi service - Performance improvements in storage.js (Bulk inserts) - Removed unnecessary logger.debug commands to increase readibility - You can now reset your initial balance by executing "node app.js -rb" --- .gitignore | 8 + README.md | 4 + app.js | 102 ++++++++--- apps/backtester.js | 43 +++-- apps/reset.js | 45 +++++ apps/trader.js | 37 ++-- backtester.js | 320 ----------------------------------- config.sample.js | 4 +- exchanges/bitstamp.js | 42 ++--- exchanges/btce.js | 42 ++--- exchanges/kraken.js | 82 +++++---- exchanges/template.js | 40 ++--- indicators/MACD.js | 16 +- indicators/PPO.js | 16 +- indicators/PSAR.js | 8 +- indicators/template.js | 8 +- package.json | 14 +- services/candleaggregator.js | 24 ++- services/dataprocessor.js | 6 +- services/dataretriever.js | 2 - services/exchangeapi.js | 2 +- services/loggingservice.js | 14 +- services/profitreporter.js | 6 +- services/simulator.js | 36 ++-- services/storage.js | 89 +++++++--- services/tradingadvisor.js | 39 +++-- util/tools.js | 26 ++- 27 files changed, 486 insertions(+), 589 deletions(-) create mode 100644 apps/reset.js delete mode 100644 backtester.js diff --git a/.gitignore b/.gitignore index b3d0573..4d476a6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,11 @@ Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ + +# All +.idea + +# Project +node_modules +logs +config.js \ No newline at end of file diff --git a/README.md b/README.md index fc2aa73..3f1c8b6 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,10 @@ If you would like to simulate trading on your collected data, execute: node app.js -b +If you would like to reset the initial balance (profit reports are based against this value), execute: + + node app.js -rb + Remember the backtester simulates your trading strategy on the data you collected. So the longer you keep the bot (app.js) running, the more significant the results of the backtester. ## Profitability diff --git a/app.js b/app.js index 00ce389..d5c0b54 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,5 @@ var _ = require('underscore'); +var cluster = require('cluster'); var loggingservice = require('./services/loggingservice.js'); @@ -6,69 +7,118 @@ var loggingservice = require('./services/loggingservice.js'); var config = require('./config.js'); //------------------------------Config -//------------------------------IntializeModules -var logger = new loggingservice('app', config.debug); -//------------------------------IntializeModules - -//------------------------------AnnounceStart -logger.log('----------------------------------------------------'); -logger.log('Starting BitBot v0.9.5'); -logger.log('Real Trading Enabled = ' + config.tradingEnabled); -logger.log('Working Dir = ' + process.cwd()); -logger.log('----------------------------------------------------'); -//------------------------------AnnounceStart - var app = function() { - _.bindAll(this, 'appListener', 'launchTrader', 'launchBacktester', 'start'); + _.bindAll(this, 'appListener', 'launchTrader', 'launchBacktester', 'launchGa', 'initializeModules', 'start'); }; app.prototype.appListener = function() { this.app.on('done', function() { - logger.log('App closed.'); + this.logger.log('App closed.'); }.bind(this)); }; app.prototype.launchTrader = function() { - logger.log('----------------------------------------------------'); - logger.log('Launching trader module.'); - logger.log('----------------------------------------------------'); + this.logger.log('----------------------------------------------------'); + this.logger.log('Launching trader module.'); + this.logger.log('----------------------------------------------------'); this.app = require('./apps/trader.js'); this.appListener(); this.app.start(); -} +}; app.prototype.launchBacktester = function() { - logger.log('----------------------------------------------------'); - logger.log('Launching backtester module.'); - logger.log('----------------------------------------------------'); + this.logger.log('----------------------------------------------------'); + this.logger.log('Launching backtester module.'); + this.logger.log('----------------------------------------------------'); this.app = require('./apps/backtester.js'); this.appListener(); this.app.start(); -} +}; + +app.prototype.launchGa = function() { + + this.logger.log('----------------------------------------------------'); + this.logger.log('Launching ga module.'); + this.logger.log('----------------------------------------------------'); + this.app = require('./apps/ga.js'); + this.appListener(); + this.app.start(); + +}; + +app.prototype.launchReset = function(collection) { + + this.logger.log('----------------------------------------------------'); + this.logger.log('Launching ga module.'); + this.logger.log('----------------------------------------------------'); + this.app = require('./apps/reset.js'); + this.appListener(); + this.app.start(collection); + +}; + +app.prototype.initializeModules = function(appName) { + + this.logger = new loggingservice(appName, config.debug); + +}; app.prototype.start = function() { var argument = process.argv[2]; if(!argument) { - this.launchTrader(); + this.appName = 'trader'; + this.run = this.launchTrader; } else { if(argument === '-b') { - this.launchBacktester(); + this.appName = 'backtester'; + this.run = this.launchBacktester; + } else if(argument === '-g') { + this.appName = 'ga'; + this.run = this.launchGa; + } else if(argument === '-rb') { + this.appName = 'reset'; + this.run = function() { + this.launchReset('balance'); + }.bind(this); + } else if(argument === '-ro') { + this.appName = 'reset'; + this.run = function() { + this.launchReset('bestOrganism'); + }.bind(this); } else { - logger.log('Invalid argument, supported options:'); - logger.log('-b: Launch Backtester'); + this.appName = 'app'; + run = null; } } + this.initializeModules(this.appName); + + //------------------------------AnnounceStart + this.logger.log('----------------------------------------------------'); + this.logger.log('Starting BitBot v0.9.6'); + this.logger.log('Real Trading Enabled = ' + config.tradingEnabled); + this.logger.log('Working Dir = ' + process.cwd()); + this.logger.log('----------------------------------------------------'); + //------------------------------AnnounceStart + + if(this.run) { + this.run(); + } else { + this.logger.log('Invalid argument, supported options:'); + this.logger.log('-b: Launch Backtester'); + this.logger.log('-rb: Reset balance'); + } + }; var application = new app(); diff --git a/apps/backtester.js b/apps/backtester.js index 4cb2207..5ef394f 100644 --- a/apps/backtester.js +++ b/apps/backtester.js @@ -12,17 +12,19 @@ var simulatorservice = require('../services/simulator.js'); var config = require('../config.js'); //------------------------------Config -//------------------------------IntializeModules +//------------------------------InitializeModules var logger = new loggingservice('backtester', config.debug); var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); -var advisor = new tradingadvisor(config.indicatorSettings, true, storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, storage, logger); var simulator = new simulatorservice(config.exchangeSettings, config.backTesterSettings, config.indicatorSettings, advisor, logger); -//------------------------------IntializeModules +//------------------------------InitializeModules var backtester = function() { - _.bindAll(this, 'start'); + this.indicatorSettings = config.indicatorSettings; + + _.bindAll(this, 'run', 'start'); }; @@ -32,19 +34,30 @@ var EventEmitter = require('events').EventEmitter; Util.inherits(backtester, EventEmitter); //---EventEmitter Setup -backtester.prototype.start = function() { +backtester.prototype.run = function() { + + advisor.setIndicator(this.indicatorSettings, false); + async.series( - { - balance: function(cb) {exchangeapi.getBalance(true, cb);}, - aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(config.indicatorSettings.candleStickSizeMinutes, cb);} - }, function(err, result) { - if(result.aggregatedCandleSticks.length > 0) { - var result = simulator.calculate(result.aggregatedCandleSticks, result.balance.fee); - simulator.report(); - this.emit('done'); - } - }.bind(this) + { + balance: function(cb) {exchangeapi.getBalance(true, cb);}, + aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(this.indicatorSettings.candleStickSizeMinutes, cb);}.bind(this) + }, function(err, result) { + if(result.aggregatedCandleSticks.length > 0) { + simulator.calculate(result.aggregatedCandleSticks, result.balance.fee, this.indicatorSettings, function(result) { + simulator.report(); + this.emit('done'); + }.bind(this)); + } + }.bind(this) ); + +}; + +backtester.prototype.start = function() { + + this.run(); + }; var backtesterApp = new backtester(); diff --git a/apps/reset.js b/apps/reset.js new file mode 100644 index 0000000..fa015b6 --- /dev/null +++ b/apps/reset.js @@ -0,0 +1,45 @@ +var _ = require('underscore'); +var tools = require('../util/tools.js'); +var async = require('async'); + +var loggingservice = require('../services/loggingservice.js'); +var storageservice = require('../services/storage.js'); +var exchangeapiservice = require('../services/exchangeapi.js'); +var tradingadvisor = require('../services/tradingadvisor.js'); +var simulatorservice = require('../services/simulator.js'); + +//------------------------------Config +var config = require('../config.js'); +//------------------------------Config + +//------------------------------InitializeModules +var logger = new loggingservice('reset', config.debug); +var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); +//------------------------------InitializeModules + +var reset = function() { + + this.indicatorSettings = config.indicatorSettings; + + _.bindAll(this, 'start'); + +}; + +//---EventEmitter Setup +var Util = require('util'); +var EventEmitter = require('events').EventEmitter; +Util.inherits(reset, EventEmitter); +//---EventEmitter Setup + +reset.prototype.start = function(collection) { + + storage.dropCollection(collection, function(err) { + logger.log(collection + ' dropped.'); + this.emit('done'); + }.bind(this)); + +}; + +var resetApp = new reset(); + +module.exports = resetApp; diff --git a/apps/trader.js b/apps/trader.js index 1e36018..d32cf66 100644 --- a/apps/trader.js +++ b/apps/trader.js @@ -1,4 +1,5 @@ var _ = require('underscore'); +var tools = require('../util/tools.js'); var loggingservice = require('../services/loggingservice.js'); var storageservice = require('../services/storage.js'); @@ -16,19 +17,19 @@ var profitreporter = require('../services/profitreporter.js'); var config = require('../config.js'); //------------------------------Config -//------------------------------IntializeModules -var logger = new loggingservice('app', config.debug); +//------------------------------InitializeModules +var logger = new loggingservice('trader', config.debug); var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); var processor = new dataprocessor(storage, logger); var aggregator = new candleaggregator(config.indicatorSettings.candleStickSizeMinutes, storage, logger); -var advisor = new tradingadvisor(config.indicatorSettings, false, storage, logger); +var advisor = new tradingadvisor(config.indicatorSettings, storage, logger); var agent = new tradingagent(config.tradingEnabled, config.exchangeSettings, storage, exchangeapi, logger); var pusher = new pushservice(config.pushOver, logger); var monitor = new ordermonitor(exchangeapi, logger); var reporter = new profitreporter(config.exchangeSettings.currencyPair, storage, exchangeapi, logger); -//------------------------------IntializeModules +//------------------------------InitializeModules var trader = function() { @@ -40,7 +41,7 @@ var trader = function() { processor.on('initialDBWrite', function(){ - reporter.start(config.resetInitialBalances); + reporter.start(); advisor.start(); @@ -68,6 +69,22 @@ var trader = function() { }); + advisor.on('advice', function(result) { + + this.logger.log('Advice: ' + result.advice + ' (' + result.indicatorValue + ')'); + + if(result.advice === 'buy') { + + agent.order(result.advice); + + } else if(result.advice === 'sell') { + + agent.order(result.advice); + + } + + }); + agent.on('realOrder',function(orderDetails){ if(config.pushOver.enabled) { @@ -110,11 +127,7 @@ var trader = function() { if(retry) { - cancelledOrderRetryTimeout = setTimeout(function(){ - - agent.order(order.orderDetails.orderType); - - }, 1000 * 5); + agent.order(order.orderDetails.orderType); } @@ -130,7 +143,7 @@ var trader = function() { _.bindAll(this, 'start', 'stop'); -} +}; //---EventEmitter Setup var Util = require('util'); @@ -148,8 +161,6 @@ trader.prototype.stop = function(cb) { retriever.stop(); - clearTimeout(cancelledOrderRetryTimeout); - monitor.resolvePreviousOrder(function() { logger.log('BitBot stopped succesfully!'); cb(); diff --git a/backtester.js b/backtester.js deleted file mode 100644 index 8576c9b..0000000 --- a/backtester.js +++ /dev/null @@ -1,320 +0,0 @@ -var _ = require('underscore'); -var tools = require('./util/tools.js'); -var moment = require('moment'); -var async = require('async'); - -var loggingservice = require('./services/loggingservice.js'); -var storageservice = require('./services/storage.js'); -var exchangeapiservice = require('./services/exchangeapi.js'); -var dataprocessor = require('./services/dataprocessor.js'); -var tradingadvisor = require('./services/tradingadvisor.js'); -var pricemonitor = require('./services/pricemonitor.js'); - -//------------------------------Config -var config = require('./config.js'); -//------------------------------Config - -//------------------------------IntializeModules -var logger = new loggingservice('backtester', config.debug); -var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); -var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); -var processor = new dataprocessor(storage, logger); -var advisor = new tradingadvisor(config.indicatorSettings, true, storage, logger); -var pricemon = new pricemonitor(config.stoplossSettings.percentageBought, config.stoplossSettings.percentageSold, config.indicatorSettings.candleStickSizeMinutes, storage, logger); -//------------------------------IntializeModules - -//------------------------------IntializeVariables -var exchange = config.exchangeSettings.exchange; -var asset = config.exchangeSettings.currencyPair.asset; -var currency = config.exchangeSettings.currencyPair.currency; -var candleStickSizeMinutes = config.indicatorSettings.candleStickSizeMinutes; -var stopLossEnabled = config.stoplossSettings.enabled; -var initialAssetBalance = config.backTesting.initialAssetBalance; -var initialCurrencyBalance = config.backTesting.initialCurrencyBalance; -var slippagePercentage = config.exchangeSettings.slippagePercentage; -var USDBalance = initialCurrencyBalance; -var BTCBalance = initialAssetBalance; -var initialBalanceSumInBTC = 0; -var initialBalanceSumInUSD = 0; -var totalBalanceInUSD = 0; -var totalBalanceInBTC = 0; -var profit = 0; -var profitPercentage = 0; -var bhProfit = 0; -var bhProfitPercentage = 0; -var transactionFee = 0; -var totalTradedVolume = 0; -var highestUSDValue = 0; -var lowestUSDValue = USDBalance; -var totalFeeCosts = 0; -var totalFeeCostsPercentage = 0; -var transactions = 0; -var slTransactions = 0; -var latestCandlePeriod; -var lastClose = 0; -var lastClosePlusSlippage = 0; -var lastCloseMinusSlippage = 0; -var csPeriod = 0; -var entryUSD = 0; -var exitUSD = 0; -var winners = 0; -var losers = 0; -var bigWinner = 0; -var bigLoser = 0; -var totalGain = 0; -var totalLoss = 0; -var averageGain = 0; -var averageLoss = 0; -var startDate; -var endDate; -//------------------------------IntializeVariables - -//------------------------------AnnounceStart -logger.log('------------------------------------------'); -logger.log('Starting BitBot Back-Tester v0.9.4'); -logger.log('Working Dir = ' + process.cwd()); -logger.log('------------------------------------------'); -//------------------------------AnnounceStart - -var createOrder = function(type, stopLoss) { - - var usableBalance = 0; - - if(type === 'buy' && USDBalance > 0) { - - entryUSD = USDBalance; - - usableBalance = tools.round(USDBalance * (1 - (transactionFee / 100)), 8); - - lastClosePlusSlippage = tools.round(lastClose * (1 + (slippagePercentage / 100)), 8); - - totalTradedVolume = tools.round(totalTradedVolume + usableBalance, 8); - - totalFeeCosts = tools.round(totalFeeCosts + (USDBalance * (transactionFee / 100)), 8); - - BTCBalance = tools.round(BTCBalance + (usableBalance / lastClosePlusSlippage), 8); - USDBalance = 0; - - var newUSDBalance = tools.round(BTCBalance * lastClosePlusSlippage, 8); - - if(newUSDBalance > highestUSDValue) { - highestUSDValue = newUSDBalance; - } else if(newUSDBalance < lowestUSDValue) { - lowestUSDValue = newUSDBalance; - } - - transactions += 1; - - if(stopLoss) { - slTransactions += 1; - logger.log('Stop loss Triggered an order:'); - } - - logger.log(new Date(latestCandlePeriod * 1000) + ' Placed buy order ' + BTCBalance + ' @ ' + lastClosePlusSlippage); - - pricemon.setPosition('bought', lastClosePlusSlippage); - advisor.setPosition({pos: 'bought', price: lastClosePlusSlippage}); - - } else if(type === 'sell' && BTCBalance > 0) { - - usableBalance = tools.round(BTCBalance * (1 - (transactionFee / 100)), 8); - - lastCloseMinusSlippage = tools.round(lastClose * (1 - (slippagePercentage / 100)), 8); - - totalTradedVolume = tools.round(totalTradedVolume + (usableBalance * lastCloseMinusSlippage), 8); - - totalFeeCosts = tools.round(totalFeeCosts + (BTCBalance * (transactionFee / 100) * lastCloseMinusSlippage), 8); - - USDBalance = tools.round(USDBalance + (usableBalance * lastCloseMinusSlippage), 8); - BTCBalance = 0; - - if(USDBalance > highestUSDValue) { - highestUSDValue = USDBalance; - } else if(USDBalance < lowestUSDValue) { - lowestUSDValue = USDBalance; - } - - exitUSD = USDBalance; - - if(entryUSD > 0) { - - var tradeResult = tools.round(exitUSD - entryUSD, 8); - - if(exitUSD > entryUSD) { - winners += 1; - totalGain = tools.round(totalGain + tradeResult, 8); - if(tradeResult > bigWinner) {bigWinner = tradeResult;} - } else { - losers += 1; - totalLoss = tools.round(totalLoss + tradeResult, 8); - if(tradeResult < bigLoser) {bigLoser = tradeResult;} - } - - } - - transactions += 1; - - if(stopLoss) { - slTransactions += 1; - logger.log('Stop loss Triggered an order:'); - } - - logger.log(new Date(latestCandlePeriod * 1000) + ' Placed sell order ' + usableBalance + ' @ ' + lastCloseMinusSlippage); - - pricemon.setPosition('sold', lastCloseMinusSlippage); - advisor.setPosition({pos: 'sold', price: lastCloseMinusSlippage}); - - } else { - - logger.debug('Wanted to place a ' + type + ' order @ ' + lastClose + ', but there are no more funds available to ' + type); - - } - -}; - -var calculate = function(err, result) { - - transactionFee = result.balance.fee; - - var loopArray = result.dbCandleSticks; - var csArray = result.aggregatedCandleSticks; - - if(loopArray.length > 0) { - - initialBalanceSumInBTC = BTCBalance + tools.round(USDBalance / _.first(loopArray).close, 8); - initialBalanceSumInUSD = USDBalance + tools.round(BTCBalance * _.first(loopArray).close, 8); - - var candleStickSizeSeconds = candleStickSizeMinutes * 60; - - if(csArray.length > 0) { - - csPeriod = _.first(csArray).period + candleStickSizeSeconds; - - } - - _.each(loopArray, function(cs) { - - lastClose = cs.close; - - if(stopLossEnabled) { - pricemon.check(cs.close); - } - - if(cs.period + 60 === csPeriod) { - - var candle = csArray.shift(); - latestCandlePeriod = candle.period; - if(csArray.length > 0) { - csPeriod = _.first(csArray).period + candleStickSizeSeconds; - } else { - csPeriod = 0; - } - - if(stopLossEnabled) { - - pricemon.update(candle, function(err) { - - logger.debug('Backtest: Created a new ' + candleStickSizeMinutes + ' minute candlestick!'); - logger.debug(JSON.stringify(candle)); - advisor.update(candle); - - }); - - } else { - - logger.debug('Backtest: Created a new ' + candleStickSizeMinutes + ' minute candlestick!'); - logger.debug(JSON.stringify(candle)); - advisor.update(candle); - - } - - } - - }); - - report(_.first(loopArray), _.last(loopArray)); - - } else { - - logger.log('No data available to run backtester on.'); - - } - -}; - -var report = function(firstCs, lastCs) { - - totalBalanceInUSD = tools.round(USDBalance + (BTCBalance * lastClose), 8); - totalBalanceInBTC = tools.round(BTCBalance + (USDBalance / lastClose), 8); - profit = tools.round(totalBalanceInUSD - initialBalanceSumInUSD, 8); - profitPercentage = tools.round(profit / initialBalanceSumInUSD * 100, 8); - totalFeeCostsPercentage = tools.round(totalFeeCosts / initialBalanceSumInUSD * 100, 8); - bhProfit = tools.round((lastCs.close - firstCs.open) * initialBalanceSumInBTC, 8); - bhProfitPercentage = tools.round(bhProfit / initialBalanceSumInUSD * 100, 8); - - if(totalBalanceInUSD > highestUSDValue) { - highestUSDValue = totalBalanceInUSD; - } - - startDate = moment(new Date(firstCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); - endDate = moment(new Date(lastCs.period*1000)).format('DD-MM-YYYY HH:mm:ss'); - - averageGain = tools.round(totalGain / winners, 8); - averageLoss = tools.round(totalLoss / losers, 8); - - logger.log('----------Report----------'); - logger.log('Exchange: ' + exchange); - logger.log('Transaction Fee: ' + transactionFee + '%'); - logger.log('Initial ' + asset + ' Balance: ' + initialAssetBalance); - logger.log('Initial ' + currency + ' Balance: ' + initialCurrencyBalance); - logger.log('Final ' + asset + ' Balance: ' + BTCBalance); - logger.log('Final ' + currency + ' Balance: ' + USDBalance); - logger.log('Total Initial Balance in ' + currency + ': ' + initialBalanceSumInUSD); - logger.log('Total Initial Balance in ' + asset + ': ' + initialBalanceSumInBTC); - logger.log('Total Final Balance in ' + currency + ': ' + totalBalanceInUSD); - logger.log('Total Final Balance in ' + asset + ': ' + totalBalanceInBTC); - logger.log('Winning trades : ' + winners + ' Losing trades: ' + losers); - logger.log('Biggest winner: ' + bigWinner + ' Biggest loser: ' + bigLoser); - logger.log('Average winner: ' + averageGain + ' Average loser: ' + averageLoss); - logger.log('Profit: ' + profit + ' (' + profitPercentage + '%)'); - logger.log('Buy and Hold Profit: ' + bhProfit + ' (' + bhProfitPercentage + '%)'); - logger.log('Lost on fees: ' + totalFeeCosts + ' (' + totalFeeCostsPercentage + '%)'); - logger.log('Total traded volue: ' + totalTradedVolume); - logger.log('Highest - Lowest ' + currency + ' Balance: ' + highestUSDValue + ' - ' + lowestUSDValue); - logger.log('Open Price: ' + firstCs.open); - logger.log('Close Price: ' + lastCs.close); - logger.log('Start - End Date: ' + startDate + ' - ' + endDate); - logger.log('Transactions: ' + transactions); - logger.log('Stop Loss Transactions: ' + slTransactions); - logger.log('--------------------------'); - -}; - -var start = function() { - async.series( - { - balance: function(cb) {exchangeapi.getBalance(true, cb);}, - dbCandleSticks: function(cb) {storage.getAllCandlesSince(0, cb);}, - aggregatedCandleSticks: function(cb) {storage.getAggregatedCandleSticks(candleStickSizeMinutes, cb);} - }, - calculate - ); -}; - -advisor.on('advice', function(advice){ - - if(advice !== 'hold') { - createOrder(advice); - } - -}); - -pricemon.on('advice', function(advice) { - - if(advice !== 'hold') { - createOrder(advice, true); - } - -}); - -start(); diff --git a/config.sample.js b/config.sample.js index e0728db..9cd448f 100644 --- a/config.sample.js +++ b/config.sample.js @@ -37,8 +37,6 @@ config.apiSettings = { config.mongoConnectionString = 'localhost/bitbot'; // The connection string for your MongoDB Installation. // Example: config.mongoConnectionString = 'username:password@example.com/mydb'; -config.resetInitialBalances = false; -// Set this to true if you want to reset the initialBalance stored by the bot. //------------------------------dbSettings //------------------------------downloaderSettings @@ -50,7 +48,7 @@ config.downloaderRefreshSeconds = 10; config.indicatorSettings = { indicator: 'MACD', // Choices: Any indicator from the indicators folder. - options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyTreshold: 0, sellTreshold: 0}, + options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyThreshold: 0, sellThreshold: 0}, // Options needed for your indicator (Look them up in the indicator's file). candleStickSizeMinutes: 5 }; diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 641d6e3..6788a48 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -38,7 +38,7 @@ exchange.prototype.retry = function(method, args) { }; -exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler, finished) { return function(err, result) { @@ -46,6 +46,8 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c var parsedError = null; + finished(); + if(err) { if(JSON.stringify(err) === '{}' && err.message) { @@ -79,14 +81,12 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var pair = this.currencyPair.pair; var handler = function(err, response) { - finish(); - if(!err) { var trades = _.map(response, function(t) { @@ -107,7 +107,7 @@ exchange.prototype.getTrades = function(retry, cb) { }; - this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); + this.bitstamp.transactions({time: 'hour'}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler, finished)); }.bind(this); @@ -119,7 +119,7 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var asset = this.currencyPair.asset; var currency = this.currencyPair.currency; @@ -128,8 +128,6 @@ exchange.prototype.getBalance = function(retry, cb) { var handler = function(err, result) { - finish(); - if(!err) { cb(null, {currencyAvailable:result.usd_available, assetAvailable:result.btc_available, fee:result.fee}); @@ -142,7 +140,7 @@ exchange.prototype.getBalance = function(retry, cb) { }; - this.bitstamp.balance(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); + this.bitstamp.balance(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler, finished)); }.bind(this); @@ -154,14 +152,12 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function (finish) { + var wrapper = function (finished) { var pair = this.currencyPair.pair; var handler = function(err, result) { - finish(); - if(!err) { var bids = _.map(result.bids, function(bid) { @@ -182,7 +178,7 @@ exchange.prototype.getOrderBook = function(retry, cb) { }; - this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); + this.bitstamp.order_book(1, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler, finished)); }.bind(this); @@ -194,14 +190,12 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var pair = this.currencyPair.pair; var handler = function(err, result) { - finish(); - if(!err) { if(!result.error) { @@ -224,11 +218,11 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { if(type === 'buy') { - this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + this.bitstamp.buy(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished)); } else if (type === 'sell') { - this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + this.bitstamp.sell(amount, price, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished)); } else { @@ -246,12 +240,10 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, result) { - finish(); - if(!err) { var open = _.find(result, function(o) { @@ -278,7 +270,7 @@ exchange.prototype.orderFilled = function(order, retry, cb) { }; - this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); + this.bitstamp.open_orders(this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler, finished)); }.bind(this); @@ -294,12 +286,10 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { if(!filled && !err) { - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, result) { - finish(); - if(!err) { if(!result.error) { @@ -316,7 +306,7 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { }; - this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); + this.bitstamp.cancel_order(order,this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler, finished)); }.bind(this); diff --git a/exchanges/btce.js b/exchanges/btce.js index a8c305a..89ab75f 100644 --- a/exchanges/btce.js +++ b/exchanges/btce.js @@ -38,7 +38,7 @@ exchange.prototype.retry = function(method, args) { }; -exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler, finished) { return function(err, result) { @@ -46,6 +46,8 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c var parsedError = null; + finished(); + if(err) { if(JSON.stringify(err) === '{}' && err.message) { @@ -79,14 +81,12 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var pair = this.currencyPair.pair.toLowerCase(); var handler = function(err, response) { - finish(); - if(!err) { var trades = _.map(response.reverse(), function(entry) { @@ -105,7 +105,7 @@ exchange.prototype.getTrades = function(retry, cb) { }; - this.btce.trades(pair, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); + this.btce.trades(pair, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler, finished)); }.bind(this); @@ -117,15 +117,13 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var asset = this.currencyPair.asset.toLowerCase(); var currency = this.currencyPair.currency.toLowerCase(); var handler = function(err, response) { - finish(); - if(!err) { cb(null, {assetAvailable: response.funds[asset], currencyAvailable: response.funds[currency], fee: 0.2}); @@ -138,7 +136,7 @@ exchange.prototype.getBalance = function(retry, cb) { }; - this.btce.getInfo(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); + this.btce.getInfo(this.errorHandler(this.getBalance, args, retry, 'getBalance', handler, finished)); }.bind(this); @@ -150,14 +148,12 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var pair = this.currencyPair.pair.toLowerCase(); var handler = function(err, response) { - finish(); - if(!err) { var bids = _.map(response.bids, function(bid) { @@ -178,7 +174,7 @@ exchange.prototype.getOrderBook = function(retry, cb) { }; - this.btce.depth(pair, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); + this.btce.depth(pair, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler, finished)); }.bind(this); @@ -190,14 +186,12 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var pair = this.currencyPair.pair.toLowerCase(); var handler = function(err, response) { - finish(); - if(!err) { var status = 'open'; @@ -220,11 +214,11 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { if(type === 'buy') { - this.btce.trade(pair, 'buy', price, amount, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + this.btce.trade(pair, 'buy', price, amount, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished)); } else if (type === 'sell') { - this.btce.trade(pair, 'sell', price, amount, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + this.btce.trade(pair, 'sell', price, amount, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished)); } else { @@ -242,12 +236,10 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - if(!err) { if(response[order]) { @@ -268,7 +260,7 @@ exchange.prototype.orderFilled = function(order, retry, cb) { }; - this.btce.orderInfo(order, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); + this.btce.orderInfo(order, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler, finished)); }.bind(this); @@ -284,12 +276,10 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { if(!filled && !err) { - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - if(!err) { if(response.order_id === order) { @@ -306,7 +296,7 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { }; - this.btce.cancelOrder(order, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); + this.btce.cancelOrder(order, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler, finished)); }.bind(this); diff --git a/exchanges/kraken.js b/exchanges/kraken.js index f9cc9ef..ce56882 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -34,11 +34,13 @@ exchange.prototype.retry = function(method, args) { // run the failed method again with the same // arguments after wait - setTimeout(function() { method.apply(self, args); }, 1000*15); + setTimeout(function() { + method.apply(self, args); + }, 1000*15); }; -exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler, finished) { return function(err, result) { @@ -46,6 +48,8 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c var parsedError = null; + finished(); + if(err) { if(JSON.stringify(err) === '{}' && err.message) { @@ -94,8 +98,6 @@ exchange.prototype.getTrades = function(retry, cb) { var handler = function(err, data) { - finished(); - if(!err) { var values = _.find(data.result, function(value, key) { @@ -120,7 +122,7 @@ exchange.prototype.getTrades = function(retry, cb) { }; - this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler)); + this.kraken.api('Trades', {"pair": pair}, this.errorHandler(this.getTrades, args, retry, 'getTrades', handler, finished)); }.bind(this); @@ -137,8 +139,6 @@ exchange.prototype.getBalance = function(retry, cb) { var asset = this.currencyPair.asset; var currency = this.currencyPair.currency; - var pair = this.currencyPair.pair; - var handler = function(err, data) { if(!err) { @@ -159,43 +159,59 @@ exchange.prototype.getBalance = function(retry, cb) { currencyValue = 0; } - var secondHandler = function(err, data) { + this.getTransactionFee(retry, function(err, result) { - finished(); + cb(null, {currencyAvailable: currencyValue, assetAvailable: assetValue, fee: result.fee}); - if(!err) { + }); - var fee = parseFloat(_.find(data.result.fees, function(value, key) { - return key === pair; - }).fee); + } else { - cb(null, {currencyAvailable:currencyValue, assetAvailable:assetValue, fee:fee}); + cb(err, null); - } else { + } - cb(err, null); + }.bind(this); - } + this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, retry, 'getBalance', handler, finished)); - }.bind(this); + }.bind(this); - this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getBalance, args, retry, 'getBalance', secondHandler)); + this.q.push({name: 'getBalance', func: wrapper}); - } else { +}; + +exchange.prototype.getTransactionFee = function(retry, cb) { + + var args = arguments; + + var wrapper = function(finished) { + + var pair = this.currencyPair.pair; + + var handler = function(err, data) { + + if (!err) { + + var fee = parseFloat(_.find(data.result.fees, function (value, key) { + return key === pair; + }).fee); - finished(); + cb(null, {fee: fee}); + + } else { cb(err, null); } - }.bind(this); + }; - this.kraken.api('Balance', {}, this.errorHandler(this.getBalance, args, retry, 'getBalance', handler)); + this.kraken.api('TradeVolume', {"pair": pair}, this.errorHandler(this.getTransactionFee, args, retry, 'getTransactionFee', handler, finished)); }.bind(this); - this.q.push({name: 'getBalance', func: wrapper}); + this.q.push({name: 'getTransactionFee', func: wrapper}); }; @@ -209,8 +225,6 @@ exchange.prototype.getOrderBook = function(retry, cb) { var handler = function(err, data) { - finished(); - if(!err) { var orderbook = _.find(data.result, function(value, key) { @@ -237,7 +251,7 @@ exchange.prototype.getOrderBook = function(retry, cb) { }; - this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler)); + this.kraken.api('Depth', {"pair": pair}, this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler, finished)); }.bind(this); @@ -255,8 +269,6 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var handler = function(err, data) { - finished(); - if(!err) { cb(null, {txid: data.result.txid[0], status: 'open'}); @@ -271,11 +283,11 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { if(type === 'buy') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'buy', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished)); } else if (type === 'sell') { - this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler)); + this.kraken.api('AddOrder', {"pair": pair, "type": 'sell', "ordertype": 'limit', "price": price, "volume": amount}, this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished)); } else { @@ -297,8 +309,6 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var handler = function(err, data) { - finished(); - if(!err) { var open = _.find(data.result.open, function(value, key) { @@ -325,7 +335,7 @@ exchange.prototype.orderFilled = function(order, retry, cb) { }; - this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler)); + this.kraken.api('OpenOrders', {}, this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler, finished)); }.bind(this); @@ -345,8 +355,6 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { var handler = function(err, data) { - finished(); - if(!err) { if(data.result.count > 0) { @@ -363,7 +371,7 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { }; - this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler)); + this.kraken.api('CancelOrder', {"txid": order}, this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler, finished)); }.bind(this); diff --git a/exchanges/template.js b/exchanges/template.js index 5afa149..efece6f 100644 --- a/exchanges/template.js +++ b/exchanges/template.js @@ -43,7 +43,7 @@ exchange.prototype.retry = function(method, args) { }; -exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler) { +exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, callerName, handler, finished) { return function(err, result) { @@ -51,6 +51,8 @@ exchange.prototype.errorHandler = function(caller, receivedArgs, retryAllowed, c var parsedError = null; + finished(); + if(err) { if(JSON.stringify(err) === '{}' && err.message) { @@ -84,18 +86,16 @@ exchange.prototype.getTrades = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - cb(null, [{date: timestamp, price: number, amount: number}]); }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(this.getTrades, args, retry, 'getTrades', handler); + this.errorHandler(this.getTrades, args, retry, 'getTrades', handler, finished); }.bind(this); @@ -107,18 +107,16 @@ exchange.prototype.getBalance = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - cb(null, {currencyAvailable: number, assetAvailable: number, fee: number}); }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(this.getBalance, args, retry, 'getBalance', handler); + this.errorHandler(this.getBalance, args, retry, 'getBalance', handler, finished); }.bind(this); @@ -130,18 +128,16 @@ exchange.prototype.getOrderBook = function(retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - cb(null, {bids: [{assetAmount: number, currencyPrice: number}], asks: [{assetAmount: number, currencyPrice: number}]}); }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler); + this.errorHandler(this.getOrderBook, args, retry, 'getOrderBook', handler, finished); }.bind(this); @@ -153,18 +149,16 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - cb(null, {txid: transaction_id, status: 'open'}); }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler); + this.errorHandler(this.placeOrder, args, retry, 'placeOrder', handler, finished); }.bind(this); @@ -176,18 +170,16 @@ exchange.prototype.orderFilled = function(order, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - cb(null, boolean); }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler); + this.errorHandler(this.orderFilled, args, retry, 'orderFilled', handler, finished); }.bind(this); @@ -199,18 +191,16 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { var args = arguments; - var wrapper = function(finish) { + var wrapper = function(finished) { var handler = function(err, response) { - finish(); - cb(null, boolean); }; // Pass this as callback to your exchange function (Expects an Err, Result output). - this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler); + this.errorHandler(this.cancelOrder, args, retry, 'cancelOrder', handler, finished); }.bind(this); diff --git a/indicators/MACD.js b/indicators/MACD.js index ef11db7..cd9899c 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -10,16 +10,16 @@ var indicator = function(options) { this.advice = 'hold'; this.length = 0; - _.bindAll(this, 'calculate', 'setPosition'); + _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); - if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyTreshold' in options || !'sellTreshold' in options) { + if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyThreshold' in options || !'sellThreshold' in options) { var err = new Error('Invalid options for MACD indicator, exiting.'); this.logger.error(err.stack); process.exit(); } // indicatorOptions - // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} + // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyThreshold: number, sellThreshold: number} }; @@ -54,11 +54,11 @@ indicator.prototype.calculate = function(cs) { this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'macd': macd, 'macdSignal': macdSignal, 'result': macdHistogram}; - if(this.previousIndicator.result <= this.options.buyTreshold && this.indicator.result > this.options.buyTreshold) { + if(this.previousIndicator.result <= this.options.buyThreshold && this.indicator.result > this.options.buyThreshold) { this.advice = 'buy'; - } else if(this.previousIndicator.result >= this.options.sellTreshold && this.indicator.result < this.options.sellTreshold) { + } else if(this.previousIndicator.result >= this.options.sellThreshold && this.indicator.result < this.options.sellThreshold) { this.advice = 'sell'; @@ -86,4 +86,10 @@ indicator.prototype.setPosition = function(pos) { }; +indicator.prototype.getPosition = function() { + + return this.position; + +}; + module.exports = indicator; diff --git a/indicators/PPO.js b/indicators/PPO.js index c8dd050..00884a3 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -10,16 +10,16 @@ var indicator = function(options) { this.advice = 'hold'; this.length = 0; - _.bindAll(this, 'calculate', 'setPosition'); + _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); - if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyTreshold' in options || !'sellTreshold' in options) { + if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyThreshold' in options || !'sellThreshold' in options) { var err = new Error('Invalid options for PPO indicator, exiting.'); this.logger.error(err.stack); process.exit(); } // indicatorOptions - // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyTreshold: number, sellTreshold: number} + // options: {neededPeriods: number, longPeriods: number, shortPeriods: number, emaPeriods: number, buyThreshold: number, sellThreshold: number} }; @@ -54,11 +54,11 @@ indicator.prototype.calculate = function(cs) { this.indicator = {'emaLong': emaLong, 'emaShort': emaShort, 'PPO': PPO, 'PPOSignal': PPOSignal, 'result': PPOHistogram}; - if(this.previousIndicator.result <= this.options.buyTreshold && this.indicator.result > this.options.buyTreshold) { + if(this.previousIndicator.result <= this.options.buyThreshold && this.indicator.result > this.options.buyThreshold) { this.advice = 'buy'; - } else if(this.previousIndicator.result >= this.options.sellTreshold && this.indicator.result < this.options.sellTreshold) { + } else if(this.previousIndicator.result >= this.options.sellThreshold && this.indicator.result < this.options.sellThreshold) { this.advice = 'sell'; @@ -86,4 +86,10 @@ indicator.prototype.setPosition = function(pos) { }; +indicator.prototype.getPosition = function() { + + return this.position; + +}; + module.exports = indicator; diff --git a/indicators/PSAR.js b/indicators/PSAR.js index de08b56..1e48176 100644 --- a/indicators/PSAR.js +++ b/indicators/PSAR.js @@ -8,7 +8,7 @@ var indicator = function(options) { this.csArray = []; this.firstCandleDone = false; - _.bindAll(this, 'calculate', 'setPosition'); + _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); if(!'AFIncrement' in options || !'maximumAF' in options) { var err = new Error('Invalid options for PSAR indicator, exiting.'); @@ -178,4 +178,10 @@ indicator.prototype.setPosition = function(pos) { }; +indicator.prototype.getPosition = function() { + + return this.position; + +}; + module.exports = indicator; diff --git a/indicators/template.js b/indicators/template.js index 006c224..df59b37 100644 --- a/indicators/template.js +++ b/indicators/template.js @@ -12,7 +12,7 @@ var indicator = function(options) { this.options = options; this.position = {}; - _.bindAll(this, 'calculate', 'setPosition'); + _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); if(!'option' in options) { var err = new Error('Invalid options for indicator, exiting.'); @@ -53,4 +53,10 @@ indicator.prototype.setPosition = function(pos) { }; +indicator.prototype.getPosition = function() { + + return this.position; + +}; + module.exports = indicator; diff --git a/package.json b/package.json index 0f72488..cf71d3a 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { "name": "BitBot", - "version": "0.9.5", + "version": "0.9.6", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { - "async": "0.9.2", - "flat": "1.3.0", + "async": "0.9.0", + "flat": "1.5.0", "bitstamp-api": "0.1.9", "mime": "1.2.11", - "moment": "2.4.0", + "moment": "2.9.0", "mongojs": "0.18.0", - "pushover-notifications": "0.1.5", + "pushover-notifications": "0.2.2", "underscore": "1.7.0", "kraken-exchange-api": "0.1.0", - "btc-e": "1.0.2", + "btc-e": "1.0.5", "winston": "0.8.3" }, "scripts": { @@ -29,5 +29,5 @@ "Bot" ], "author": "Ruben Callewaert", - "license": "MIT" + "license": "Apache License" } diff --git a/services/candleaggregator.js b/services/candleaggregator.js index d7d3424..950a532 100644 --- a/services/candleaggregator.js +++ b/services/candleaggregator.js @@ -4,7 +4,7 @@ var tools = require('../util/tools.js'); var aggregator = function(candleStickSizeMinutes, storage, logger) { this.storage = storage; - this.candleStickSize = candleStickSizeMinutes; + this.candleStickSizeMinutes = candleStickSizeMinutes; this.logger = logger; this.previousCompleteCandleStickPeriod = 0; @@ -20,7 +20,7 @@ Util.inherits(aggregator, EventEmitter); aggregator.prototype.update = function() { - this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSize, function(err, completeCandleStick) { + this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSizeMinutes, function(err, completeCandleStick) { if(completeCandleStick) { @@ -32,12 +32,12 @@ aggregator.prototype.update = function() { if(completeCandleStick.period !== this.previousCompleteCandleStickPeriod) { - this.logger.log('Created a new ' + this.candleStickSize + ' minute candlestick!'); + this.logger.log('Created a new ' + this.candleStickSizeMinutes + ' minute candlestick!'); this.logger.log(JSON.stringify(completeCandleStick)); this.previousCompleteCandleStickPeriod = completeCandleStick.period; - this.storage.removeOldDBCandles(this.candleStickSize, function(err) { + this.storage.removeOldDBCandles(this.candleStickSizeMinutes, function(err) { this.emit('update', completeCandleStick); @@ -51,4 +51,20 @@ aggregator.prototype.update = function() { }; +aggregator.prototype.setCandleStickSize = function(candleStickSizeMinutes) { + + this.candleStickSizeMinutes = candleStickSizeMinutes; + + this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSizeMinutes, function(err, completeCandleStick) { + + if(completeCandleStick) { + + this.previousCompleteCandleStickPeriod = completeCandleStick.period; + + } + + }); + +}; + module.exports = aggregator; diff --git a/services/dataprocessor.js b/services/dataprocessor.js index ac5e494..183e1b3 100644 --- a/services/dataprocessor.js +++ b/services/dataprocessor.js @@ -152,11 +152,7 @@ processor.prototype.createCandleSticks = function(ticks, callback) { } - async.eachSeries(toBePushed, function(candleStick, nextPush) { - - this.storage.push(candleStick, nextPush); - - }.bind(this), callback); + this.storage.push(toBePushed, callback); }.bind(this)); diff --git a/services/dataretriever.js b/services/dataretriever.js index 7dbdb37..fd201ed 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -43,11 +43,9 @@ downloader.prototype.start = function() { this.logger.log('Downloader started!'); - this.logger.debug('Retrieving new trades from exchange API.'); this.exchangeapi.getTrades(false, this.processTrades); this.downloadInterval = setInterval(function(){ - this.logger.debug('Retrieving new trades from exchange API.'); this.exchangeapi.getTrades(false, this.processTrades); }.bind(this),1000 * this.refreshInterval); diff --git a/services/exchangeapi.js b/services/exchangeapi.js index 80c89a3..1dd4bd7 100644 --- a/services/exchangeapi.js +++ b/services/exchangeapi.js @@ -22,7 +22,7 @@ var api = function(exchangeSettings, apiSettings, logger) { } catch(err) { - var err = new Error('Wrong exchange chosen. This exchange doesn\'t exist.'); + this.logger.error('Wrong exchange chosen. This exchange doesn\'t exist.'); this.logger.error(err.stack); process.exit(); diff --git a/services/loggingservice.js b/services/loggingservice.js index d12d4a7..dac0137 100644 --- a/services/loggingservice.js +++ b/services/loggingservice.js @@ -3,7 +3,13 @@ var _ = require('underscore'); var winston = require('winston'); var fs = require('fs'); -var logger = function(app, debug) { +var logger = function(app, debug, prefix) { + + if(prefix) { + this.prefix = prefix + ': '; + } else { + this.prefix = ''; + } this.debugEnabled = debug; @@ -44,7 +50,7 @@ var logger = function(app, debug) { logger.prototype.log = function(message) { - this.logger.log('INFO', message); + this.logger.log('INFO', this.prefix + message); }; @@ -52,7 +58,7 @@ logger.prototype.debug = function(message) { if(this.debugEnabled) { - this.logger.log('DEBUG', message); + this.logger.log('DEBUG', this.prefix + message); } @@ -60,7 +66,7 @@ logger.prototype.debug = function(message) { logger.prototype.error = function(message) { - this.logger.log('ERROR', message); + this.logger.log('ERROR', this.prefix + message); }; diff --git a/services/profitreporter.js b/services/profitreporter.js index db14b9d..5458188 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -79,9 +79,7 @@ reporter.prototype.processBalance = function(err, result) { }; -reporter.prototype.start = function(resetInitialBalances) { - - this.resetInitialBalances = resetInitialBalances; +reporter.prototype.start = function() { this.storage.getInitialBalance(function(err, result) { @@ -94,7 +92,7 @@ reporter.prototype.start = function(resetInitialBalances) { } else { - if(result && !resetInitialBalances) { + if(result) { this.initalTotalCurrencyBalance = result; diff --git a/services/simulator.js b/services/simulator.js index e2ed440..d3d058a 100644 --- a/services/simulator.js +++ b/services/simulator.js @@ -58,7 +58,7 @@ var simulator = function(exchangeSettings, backTesterSettings, indicatorSettings }; -simulator.prototype.calculate = function(csArray, transactionFee) { +simulator.prototype.calculate = function(csArray, transactionFee, indicatorSettings, callback) { // Set Variables this.options.currencyBalance = this.options.initialCurrencyBalance; @@ -75,6 +75,7 @@ simulator.prototype.calculate = function(csArray, transactionFee) { this.options.totalGain = 0; this.options.totalLoss = 0; this.options.transactions = []; + this.indicatorSettings = indicatorSettings; // Set Variables this.options.transactionFee = transactionFee; @@ -85,26 +86,22 @@ simulator.prototype.calculate = function(csArray, transactionFee) { this.options.firstCs = _.first(csArray); this.options.lastCs = _.last(csArray); - _.forEach(csArray, function(cs) { + _.each(csArray, function(cs) { this.options.latestCandlePeriod = cs.period; this.options.lastClose = cs.close; - this.logger.debug('Backtest: Created a new ' + this.options.candleStickSizeMinutes + ' minute candlestick!'); - this.logger.debug(JSON.stringify(cs)); + var result = this.advisor.update(cs); - var advice = this.advisor.update(cs); - - if(advice !== 'hold') { - this.createOrder(advice); + if (result.advice !== 'hold') { + this.createOrder(result.advice); } }.bind(this)); this.postProcess(); - - return this.options; + callback(this.options); }; @@ -131,7 +128,9 @@ simulator.prototype.postProcess = function() { this.options.averageGain = tools.round(this.options.totalGain / this.options.winners, 8); this.options.averageLoss = tools.round(this.options.totalLoss / this.options.losers, 8); -} + return this.options; + +}; simulator.prototype.createOrder = function(type) { @@ -158,7 +157,6 @@ simulator.prototype.createOrder = function(type) { this.options.lowestCurrencyValue = this.options.newcurrencyBalance; } - this.logger.debug(new Date(this.options.latestCandlePeriod * 1000) + ' Placed buy order ' + this.options.assetBalance + ' @ ' + this.options.lastClosePlusSlippage); this.options.transactions.push(new Date(this.options.latestCandlePeriod * 1000) + ' Placed buy order ' + this.options.assetBalance + ' @ ' + this.options.lastClosePlusSlippage); this.advisor.setPosition({pos: 'bought', price: this.options.lastClosePlusSlippage}); @@ -200,15 +198,10 @@ simulator.prototype.createOrder = function(type) { } - this.logger.debug(new Date(this.options.latestCandlePeriod * 1000) + ' Placed sell order ' + this.options.usableBalance + ' @ ' + this.options.lastCloseMinusSlippage); this.options.transactions.push(new Date(this.options.latestCandlePeriod * 1000) + ' Placed sell order ' + this.options.usableBalance + ' @ ' + this.options.lastCloseMinusSlippage); this.advisor.setPosition({pos: 'sold', price: this.options.lastCloseMinusSlippage}); - } else { - - this.logger.debug('Wanted to place a ' + type + ' order @ ' + this.options.lastClose + ', but there are no more funds available to ' + type); - } }; @@ -217,9 +210,12 @@ simulator.prototype.report = function() { this.options.transactions.forEach(function(transaction) { this.logger.log(transaction); - }.bind(this)) + }.bind(this)); + + this.logger.log('--------------Settings--------------'); + JSON.stringify(this.indicatorSettings, undefined, 2).split(/\n/).forEach(function(line){this.logger.log(line)}.bind(this)); - this.logger.log('----------Report----------'); + this.logger.log('---------------Report---------------'); this.logger.log('Exchange: ' + this.options.exchange); this.logger.log('Transaction Fee: ' + this.options.transactionFee + '%'); this.logger.log('Initial ' + this.options.asset + ' Balance: ' + this.options.initialAssetBalance); @@ -242,7 +238,7 @@ simulator.prototype.report = function() { this.logger.log('Close Price: ' + this.options.closePrice); this.logger.log('Start - End Date: ' + this.options.startDate + ' - ' + this.options.endDate); this.logger.log('Transactions: ' + this.options.transactions.length); - this.logger.log('--------------------------'); + this.logger.log('------------------------------------'); }; diff --git a/services/storage.js b/services/storage.js index 92614a8..f92c34c 100644 --- a/services/storage.js +++ b/services/storage.js @@ -7,20 +7,26 @@ var storage = function(exchangeSettings, mongoConnectionString, logger) { this.pair = exchangeSettings.currencyPair.pair; this.exchange = exchangeSettings.exchange; - this.dbCollectionName = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; + this.exchangePair = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; this.mongoConnectionString = mongoConnectionString; this.logger = logger; - _.bindAll(this, 'push', 'getLastNCandles', 'getAllCandles', 'getAllCandlesSince', 'getLastClose', 'getLastNonEmptyPeriod', 'getLastNonEmptyClose', 'getLastNCompleteAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getCompleteAggregatedCandleSticks', 'getLastNAggregatedCandleSticks', 'getAggregatedCandleSticks', 'calculateAggregatedCandleStick', 'aggregateCandleSticks', 'removeOldDBCandles', 'getInitialBalance', 'setInitialBalance'); + _.bindAll(this, 'push', 'getLastNCandles', 'getAllCandles', 'getAllCandlesSince', 'getLastClose', 'getLastNonEmptyPeriod', 'getLastNonEmptyClose', 'getLastNCompleteAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getCompleteAggregatedCandleSticks', 'getLastNAggregatedCandleSticks', 'getAggregatedCandleSticks', 'getAggregatedCandleSticksSince', 'calculateAggregatedCandleStick', 'aggregateCandleSticks', 'removeOldDBCandles', 'dropCollection', 'getInitialBalance', 'setInitialBalance'); }; -storage.prototype.push = function(cs, callback) { +storage.prototype.push = function(csArray, callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.update({period: cs.period}, cs, { upsert: true }, function(err, result) { + var bulk = csCollection.initializeOrderedBulkOp(); + + _.forEach(csArray, function(cs) { + bulk.find({period: cs.period}).upsert().updateOne(cs); + }); + + bulk.execute(function(err, res) { csDatastore.close(); @@ -41,7 +47,7 @@ storage.prototype.push = function(cs, callback) { storage.prototype.getLastNCandles = function(N, callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); csCollection.find({}).sort({period:-1}).limit(N, function(err, candlesSticks) { @@ -65,7 +71,7 @@ storage.prototype.getLastNCandles = function(N, callback) { storage.prototype.getAllCandles = function(callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); csCollection.find({}).sort({period:1}, function(err, candlesSticks) { @@ -89,7 +95,7 @@ storage.prototype.getAllCandles = function(callback) { storage.prototype.getAllCandlesSince = function(period, callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); csCollection.find({period: { $gte: period }}).sort({period:1}, function(err, candlesSticks) { @@ -113,7 +119,7 @@ storage.prototype.getAllCandlesSince = function(period, callback) { storage.prototype.getLastClose = function(callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); csCollection.find({}).sort({period:-1}).limit(1, function(err, candleSticks) { @@ -140,7 +146,7 @@ storage.prototype.getLastClose = function(callback) { storage.prototype.getLastNonEmptyPeriod = function(callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { @@ -167,7 +173,7 @@ storage.prototype.getLastNonEmptyPeriod = function(callback) { storage.prototype.getLastNonEmptyClose = function(callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { @@ -265,6 +271,26 @@ storage.prototype.getAggregatedCandleSticks = function(candleStickSize, callback }; +storage.prototype.getAggregatedCandleSticksSince = function(candleStickSize, period, callback) { + + this.getAllCandlesSince(period, function(err, candleSticks) { + + if(candleSticks.length > 0) { + + var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); + + callback(null, aggregatedCandleSticks); + + } else { + + callback(null, []); + + } + + }.bind(this)); + +}; + storage.prototype.calculateAggregatedCandleStick = function(period, relevantSticks) { var currentCandleStick = {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; @@ -337,7 +363,7 @@ storage.prototype.aggregateCandleSticks = function(candleStickSize, candleSticks storage.prototype.removeOldDBCandles = function(candleStickSize, callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.dbCollectionName); + var csCollection = csDatastore.collection(this.exchangePair); var candleStickSizeSeconds = candleStickSize * 60; @@ -354,28 +380,37 @@ storage.prototype.removeOldDBCandles = function(candleStickSize, callback) { }; -storage.prototype.getInitialBalance = function(callback) { +storage.prototype.dropCollection = function(collection, callback) { var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection('balance'); + var csCollection = csDatastore.collection(collection); - csCollection.find({exchangePair: this.dbCollectionName}).limit(1, function(err, balance) { + csCollection.drop(function(err) { csDatastore.close(); - if(err) { + callback(err); - callback(err); + }); - } else if(balance.length > 0 ){ +}; - var initialBalance = balance[0].initialBalance; +storage.prototype.setInitialBalance = function(initialBalance, callback) { - callback(null, initialBalance); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); + + csCollection.update({exchangePair: this.exchangePair}, {exchangePair: this.exchangePair, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { + + csDatastore.close(); + + if(err) { + + callback(err); } else { - callback(null, null); + callback(null); } @@ -383,12 +418,12 @@ storage.prototype.getInitialBalance = function(callback) { }; -storage.prototype.setInitialBalance = function(initialBalance, callback) { +storage.prototype.getInitialBalance = function(callback) { var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection('balance'); - csCollection.update({exchangePair: this.dbCollectionName}, {exchangePair: this.dbCollectionName, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { + csCollection.find({exchangePair: this.exchangePair}).limit(1, function(err, doc) { csDatastore.close(); @@ -396,9 +431,15 @@ storage.prototype.setInitialBalance = function(initialBalance, callback) { callback(err); + } else if(doc.length > 0 ){ + + var initialBalance = doc[0].initialBalance; + + callback(null, initialBalance); + } else { - callback(null); + callback(null, null); } diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index 600782d..9306373 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -2,10 +2,9 @@ var _ = require('underscore'); var async = require('async'); var fs = require('fs'); -var advisor = function(indicatorSettings, backTesting, storage, logger) { +var advisor = function(indicatorSettings, storage, logger) { this.candleStickSize = indicatorSettings.candleStickSizeMinutes; - this.backTesting = backTesting; this.storage = storage; this.logger = logger; @@ -24,7 +23,7 @@ var advisor = function(indicatorSettings, backTesting, storage, logger) { } catch(err) { - var err = new Error('Wrong indicator chosen. This indicator doesn\'t exist.'); + this.logger.error('Wrong indicator chosen. This indicator doesn\'t exist.'); this.logger.error(err.stack); process.exit(); @@ -34,11 +33,11 @@ var advisor = function(indicatorSettings, backTesting, storage, logger) { }; -/*//---EventEmitter Setup +//---EventEmitter Setup var Util = require('util'); var EventEmitter = require('events').EventEmitter; Util.inherits(advisor, EventEmitter); -//---EventEmitter Setup*/ +//---EventEmitter Setup advisor.prototype.start = function() { @@ -48,8 +47,20 @@ advisor.prototype.start = function() { var result = this.selectedIndicator.calculate(candleSticks[i]); + if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { + if(['buy', 'sell'].indexOf(result.advice) >= 0) { + this.latestTradeAdvice = result; + } + } else { + var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); + this.logger.error(err.stack); + process.exit(); + } + } + this.emit('advice', this.latestTradeAdvice ); + }.bind(this)); }; @@ -58,15 +69,9 @@ advisor.prototype.update = function(cs) { var result = this.selectedIndicator.calculate(cs); - if(!this.backTesting) { - this.logger.log('Advice: ' + result.advice + ' (' + result.indicatorValue + ')'); - } else { - this.logger.debug('Advice: ' + result.advice + ' (' + result.indicatorValue + ')'); - } - if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { - //this.emit('advice', result.advice); - return result.advice; + this.emit('advice', result); + return result; } else { var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); this.logger.error(err.stack); @@ -81,10 +86,16 @@ advisor.prototype.setPosition = function(pos) { }; -advisor.prototype.setIndicator = function(indicatorSettings) { +advisor.prototype.setIndicator = function(indicatorSettings, updateIndicator) { this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); + if(updateIndicator) { + + this.start(); + + } + }; module.exports = advisor; diff --git a/util/tools.js b/util/tools.js index e5521fa..1ece6c8 100644 --- a/util/tools.js +++ b/util/tools.js @@ -44,7 +44,31 @@ tools.prototype.rangeToArray = function(range) { return result; -} +}; + +tools.prototype.runEvery = function(ms, func) { + + var timeout; + + var loopFunc = function() { + + var now = new Date().getTime(); + var next = (now - (now % ms) + ms) - now; + + timeout = setTimeout(func, next); + + }; + + loopFunc(); + + var interval = setInterval(loopFunc, ms); + + return function() { + clearInterval(interval); + clearTimeout(timeout); + }; + +}; var utiltools = new tools(); From 9dedfda8ca4b3d3085c6f88fc3fa403c2a27a3c7 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 19 Jan 2015 00:11:44 +0100 Subject: [PATCH 43/57] tools.js Documentation --- indicators/MACD.js | 8 +----- indicators/PPO.js | 8 +----- indicators/PSAR.js | 8 +----- indicators/template.js | 8 +----- util/tools.js | 58 +++++++++++++++++++++++++++++++++++------- 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/indicators/MACD.js b/indicators/MACD.js index cd9899c..7de6ccf 100644 --- a/indicators/MACD.js +++ b/indicators/MACD.js @@ -10,7 +10,7 @@ var indicator = function(options) { this.advice = 'hold'; this.length = 0; - _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); + _.bindAll(this, 'calculate', 'setPosition'); if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyThreshold' in options || !'sellThreshold' in options) { var err = new Error('Invalid options for MACD indicator, exiting.'); @@ -86,10 +86,4 @@ indicator.prototype.setPosition = function(pos) { }; -indicator.prototype.getPosition = function() { - - return this.position; - -}; - module.exports = indicator; diff --git a/indicators/PPO.js b/indicators/PPO.js index 00884a3..aa58472 100644 --- a/indicators/PPO.js +++ b/indicators/PPO.js @@ -10,7 +10,7 @@ var indicator = function(options) { this.advice = 'hold'; this.length = 0; - _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); + _.bindAll(this, 'calculate', 'setPosition'); if(!'neededPeriods' in options || !'longPeriods' in options || !'shortPeriods' in options || !'emaPeriods' in options || !'buyThreshold' in options || !'sellThreshold' in options) { var err = new Error('Invalid options for PPO indicator, exiting.'); @@ -86,10 +86,4 @@ indicator.prototype.setPosition = function(pos) { }; -indicator.prototype.getPosition = function() { - - return this.position; - -}; - module.exports = indicator; diff --git a/indicators/PSAR.js b/indicators/PSAR.js index 1e48176..de08b56 100644 --- a/indicators/PSAR.js +++ b/indicators/PSAR.js @@ -8,7 +8,7 @@ var indicator = function(options) { this.csArray = []; this.firstCandleDone = false; - _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); + _.bindAll(this, 'calculate', 'setPosition'); if(!'AFIncrement' in options || !'maximumAF' in options) { var err = new Error('Invalid options for PSAR indicator, exiting.'); @@ -178,10 +178,4 @@ indicator.prototype.setPosition = function(pos) { }; -indicator.prototype.getPosition = function() { - - return this.position; - -}; - module.exports = indicator; diff --git a/indicators/template.js b/indicators/template.js index df59b37..006c224 100644 --- a/indicators/template.js +++ b/indicators/template.js @@ -12,7 +12,7 @@ var indicator = function(options) { this.options = options; this.position = {}; - _.bindAll(this, 'calculate', 'setPosition', 'getPosition'); + _.bindAll(this, 'calculate', 'setPosition'); if(!'option' in options) { var err = new Error('Invalid options for indicator, exiting.'); @@ -53,10 +53,4 @@ indicator.prototype.setPosition = function(pos) { }; -indicator.prototype.getPosition = function() { - - return this.position; - -}; - module.exports = indicator; diff --git a/util/tools.js b/util/tools.js index 1ece6c8..4134d90 100644 --- a/util/tools.js +++ b/util/tools.js @@ -1,21 +1,44 @@ var tools = function() { - }; -tools.prototype.unixTimeStamp = function(timestamp) { +/** + * Returns a JS timestamp as a unix timestamp (convert MS to S). + * @param timestamp + * @returns {number} + */ +tools.prototype.unixTimeStamp = function unixTimeStamp(timestamp) { return Math.floor(timestamp/1000); }; -tools.prototype.getRandomInt = function(min, max) { +/** + * Returns a random integer between min and max values. + * @param min + * @param max + * @returns {number} + */ +tools.prototype.getRandomInt = function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }; -tools.prototype.getRandomArbitrary = function(decimals, min, max) { - return (Math.random() * (max - min) + min).toFixed(decimals); +/** + * Returns a random number between min and max values with a specified number of decimals. + * @param decimals + * @param min + * @param max + * @returns {number} + */ +tools.prototype.getRandomArbitrary = function getRandomArbitrary(decimals, min, max) { + return Number((Math.random() * (max - min) + min).toFixed(decimals)); }; -tools.prototype.round = function(value, decimals) { +/** + * Rounds a value up to an amount of decimals and returns it. + * @param value + * @param decimals + * @returns {number} + */ +tools.prototype.round = function round(value, decimals) { // Shift value = value.toString().split('e'); value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] + decimals) : decimals))); @@ -24,7 +47,13 @@ tools.prototype.round = function(value, decimals) { return Number((value[0] + 'e' + (value[1] ? (+value[1] - decimals) : -decimals))); }; -tools.prototype.floor = function(value, decimals) { +/** + * Floors a value up to an amount of decimals and returns it. + * @param value + * @param decimals + * @returns {number} + */ +tools.prototype.floor = function floor(value, decimals) { // Shift value = value.toString().split('e'); value = Math.floor(+(value[0] + 'e' + (value[1] ? (+value[1] + decimals) : decimals))); @@ -33,7 +62,12 @@ tools.prototype.floor = function(value, decimals) { return Number((value[0] + 'e' + (value[1] ? (+value[1] - decimals) : -decimals))); }; -tools.prototype.rangeToArray = function(range) { +/** + * Converts a range to a complete array and returns it. + * @param {Array} range Array is defined as [incrementDecimals, startValue, endValue]. + * @returns {Array} + */ +tools.prototype.rangeToArray = function rangeToArray(range) { var result = []; var increment = this.floor(1 / Math.pow(10,range[0]), range[0]); @@ -46,7 +80,13 @@ tools.prototype.rangeToArray = function(range) { }; -tools.prototype.runEvery = function(ms, func) { +/** + * Run a specified function at a specified interval and at the exact start of that interval and returns a function to cancel the interval. + * @param ms + * @param func + * @returns {Function} Function to cancel the set interval. + */ +tools.prototype.runEvery = function runEvery(ms, func) { var timeout; From 024d325023974aad3fdd19685af7480507fa1452 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Mon, 19 Jan 2015 16:45:35 +0100 Subject: [PATCH 44/57] Bugfix for the BTC-E exchange --- apps/backtester.js | 2 +- exchanges/btce.js | 10 +++++----- services/tradingadvisor.js | 12 ++++-------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/apps/backtester.js b/apps/backtester.js index 5ef394f..d51fdf6 100644 --- a/apps/backtester.js +++ b/apps/backtester.js @@ -36,7 +36,7 @@ Util.inherits(backtester, EventEmitter); backtester.prototype.run = function() { - advisor.setIndicator(this.indicatorSettings, false); + advisor.setIndicator(this.indicatorSettings); async.series( { diff --git a/exchanges/btce.js b/exchanges/btce.js index 89ab75f..bd782f6 100644 --- a/exchanges/btce.js +++ b/exchanges/btce.js @@ -126,7 +126,7 @@ exchange.prototype.getBalance = function(retry, cb) { if(!err) { - cb(null, {assetAvailable: response.funds[asset], currencyAvailable: response.funds[currency], fee: 0.2}); + cb(null, {assetAvailable: response.return.funds[asset], currencyAvailable: response.return.funds[currency], fee: 0.2}); } else { @@ -196,13 +196,13 @@ exchange.prototype.placeOrder = function(type, amount, price, retry, cb) { var status = 'open'; - if(response.order_id === 0) { + if(response.return.order_id === 0) { status = 'filled'; } - cb(null, {txid: response.order_id, status: status}); + cb(null, {txid: response.return.order_id, status: status}); } else { @@ -242,7 +242,7 @@ exchange.prototype.orderFilled = function(order, retry, cb) { if(!err) { - if(response[order]) { + if(response.return[order]) { cb(null, false); @@ -282,7 +282,7 @@ exchange.prototype.cancelOrder = function(order, retry, cb) { if(!err) { - if(response.order_id === order) { + if(response.return.order_id === order) { cb(null, true); } else { cb(null, false); diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index 9306373..4107d0e 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -59,7 +59,9 @@ advisor.prototype.start = function() { } - this.emit('advice', this.latestTradeAdvice ); + if(['buy', 'sell'].indexOf(this.latestTradeAdvice) >= 0) { + this.emit('advice', this.latestTradeAdvice); + } }.bind(this)); @@ -86,16 +88,10 @@ advisor.prototype.setPosition = function(pos) { }; -advisor.prototype.setIndicator = function(indicatorSettings, updateIndicator) { +advisor.prototype.setIndicator = function(indicatorSettings) { this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); - if(updateIndicator) { - - this.start(); - - } - }; module.exports = advisor; From 8d82b7ae3f27e769a6841cf50b4506d7e60ceeaa Mon Sep 17 00:00:00 2001 From: Ruben Callewaert Date: Tue, 20 Jan 2015 00:50:51 +0100 Subject: [PATCH 45/57] v0.9.7 Trading disabled mode now simulates trades - All remaining BTC-E bugs should be fixed - When running with trading disabled, BitBot will generate fake trades and manage a fake balance. --- app.js | 2 +- apps/trader.js | 4 +- package.json | 2 +- services/candleaggregator.js | 4 +- services/profitreporter.js | 72 ++++++++++++++++++------------- services/tradingadvisor.js | 10 ++++- services/tradingagent.js | 83 +++++++++++++++++++++++++++--------- 7 files changed, 118 insertions(+), 59 deletions(-) diff --git a/app.js b/app.js index d5c0b54..5ef90ac 100644 --- a/app.js +++ b/app.js @@ -105,7 +105,7 @@ app.prototype.start = function() { //------------------------------AnnounceStart this.logger.log('----------------------------------------------------'); - this.logger.log('Starting BitBot v0.9.6'); + this.logger.log('Starting BitBot v0.9.7'); this.logger.log('Real Trading Enabled = ' + config.tradingEnabled); this.logger.log('Working Dir = ' + process.cwd()); this.logger.log('----------------------------------------------------'); diff --git a/apps/trader.js b/apps/trader.js index d32cf66..09ba7ce 100644 --- a/apps/trader.js +++ b/apps/trader.js @@ -117,13 +117,13 @@ var trader = function() { } - reporter.updateBalance(true); + reporter.updateBalance(true, order); }); monitor.on('cancelled', function(order, retry) { - reporter.updateBalance(false); + reporter.updateBalance(false, order); if(retry) { diff --git a/package.json b/package.json index cf71d3a..cd2bd33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BitBot", - "version": "0.9.6", + "version": "0.9.7", "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { diff --git a/services/candleaggregator.js b/services/candleaggregator.js index 950a532..6aba9af 100644 --- a/services/candleaggregator.js +++ b/services/candleaggregator.js @@ -8,7 +8,7 @@ var aggregator = function(candleStickSizeMinutes, storage, logger) { this.logger = logger; this.previousCompleteCandleStickPeriod = 0; - _.bindAll(this, 'update'); + _.bindAll(this, 'update', 'setCandleStickSize'); }; @@ -63,7 +63,7 @@ aggregator.prototype.setCandleStickSize = function(candleStickSizeMinutes) { } - }); + }.bind(this)); }; diff --git a/services/profitreporter.js b/services/profitreporter.js index 5458188..045d8a6 100644 --- a/services/profitreporter.js +++ b/services/profitreporter.js @@ -9,7 +9,10 @@ var reporter = function(currencyPair, storage, exchangeapi, logger) { this.exchangeapi = exchangeapi; this.logger = logger; - _.bindAll(this, 'intialize', 'createReport', 'processBalance', 'start', 'updateBalance'); + this.currencyBalance = 0; + this.assetBalance = 0; + + _.bindAll(this, 'initialize', 'createReport', 'processBalance', 'start', 'updateBalance'); }; @@ -19,15 +22,15 @@ var EventEmitter = require('events').EventEmitter; Util.inherits(reporter, EventEmitter); //---EventEmitter Setup -reporter.prototype.intialize = function(err, result) { +reporter.prototype.initialize = function(err, result) { - this.currencyBalance = parseFloat(result.balance.currencyAvailable); - this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 8); + var currencyBalance = parseFloat(result.balance.currencyAvailable); + var assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 8); - this.highestBid = _.first(result.orderBook.bids).currencyPrice; - this.assetBalanceInCurrency = this.assetBalance * this.highestBid; + var highestBid = _.first(result.orderBook.bids).currencyPrice; + var assetBalanceInCurrency = assetBalance * highestBid; - this.initalTotalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 8); + this.initalTotalCurrencyBalance = tools.round(currencyBalance + assetBalanceInCurrency, 8); this.storage.setInitialBalance(this.initalTotalCurrencyBalance, function(err) { @@ -38,13 +41,6 @@ reporter.prototype.intialize = function(err, result) { process.exit(); - } else { - - if(this.resetInitialBalances) { - this.logger.log(this.currencyPair.pair + ' Balance reset successfully, change the configuration setting back to false and restart the application.'); - process.exit(); - } - } }.bind(this)); @@ -61,19 +57,19 @@ reporter.prototype.createReport = function() { }; -reporter.prototype.processBalance = function(err, result) { +reporter.prototype.processBalance = function(err, result, includeReport, order) { this.currencyBalance = parseFloat(result.balance.currencyAvailable); this.assetBalance = tools.round(parseFloat(result.balance.assetAvailable), 8); - this.highestBid = _.first(result.orderBook.bids).currencyPrice; - this.assetBalanceInCurrency = this.assetBalance * this.highestBid; + var highestBid = _.first(result.orderBook.bids).currencyPrice; + var assetBalanceInCurrency = this.assetBalance * highestBid; - this.totalCurrencyBalance = tools.round(this.currencyBalance + this.assetBalanceInCurrency, 8); - this.profitAbsolute = this.totalCurrencyBalance - this.initalTotalCurrencyBalance; + this.totalCurrencyBalance = tools.round(this.currencyBalance + assetBalanceInCurrency, 8); + this.profitAbsolute = tools.round(this.totalCurrencyBalance - this.initalTotalCurrencyBalance, 8); this.profitPercentage = tools.round((this.profitAbsolute / this.initalTotalCurrencyBalance) * 100, 8); - if(this.includeReport) { + if(includeReport) { this.createReport(); } @@ -103,7 +99,7 @@ reporter.prototype.start = function() { balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this) }, - this.intialize + this.initialize ); } @@ -114,17 +110,33 @@ reporter.prototype.start = function() { }; -reporter.prototype.updateBalance = function(includeReport) { +reporter.prototype.updateBalance = function(includeReport, order) { - this.includeReport = includeReport; + if(order.orderDetails.order != 'Simulated') { - async.series( - { - balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), - orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this) - }, - this.processBalance - ); + async.series( + { + balance: function (cb) { + this.exchangeapi.getBalance(true, cb); + }.bind(this), + orderBook: function (cb) { + this.exchangeapi.getOrderBook(true, cb); + }.bind(this) + }, + function (err, result) { + this.processBalance(err, result, includeReport, order); + }.bind(this) + ); + + } else { + + this.exchangeapi.getOrderBook(true, function(err, result) { + + this.processBalance(err, {balance: {currencyAvailable: order.orderDetails.simulationBalance.currencyAvailable, assetAvailable: order.orderDetails.simulationBalance.assetAvailable}, orderBook: result}, includeReport, order); + + }.bind(this)); + + } }; diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index 4107d0e..ac08fc3 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -39,10 +39,12 @@ var EventEmitter = require('events').EventEmitter; Util.inherits(advisor, EventEmitter); //---EventEmitter Setup -advisor.prototype.start = function() { +advisor.prototype.start = function(callback) { this.storage.getLastNCompleteAggregatedCandleSticks(1000, this.candleStickSize, function(err, candleSticks) { + this.latestTradeAdvice = {advice: 'hold'}; + for(var i = 0; i < candleSticks.length; i++) { var result = this.selectedIndicator.calculate(candleSticks[i]); @@ -59,10 +61,14 @@ advisor.prototype.start = function() { } - if(['buy', 'sell'].indexOf(this.latestTradeAdvice) >= 0) { + if(['buy', 'sell'].indexOf(this.latestTradeAdvice.advice) >= 0) { this.emit('advice', this.latestTradeAdvice); } + if(callback) { + callback(); + } + }.bind(this)); }; diff --git a/services/tradingagent.js b/services/tradingagent.js index 38d8e5e..299543d 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -6,11 +6,12 @@ var agent = function(tradingEnabled, exchangeSettings, storage, exchangeapi, log _.bindAll(this, 'order', 'calculateOrder', 'placeRealOrder', 'placeSimulatedOrder', 'processOrder'); - this.tradingEnabled = tradingEnabled; - this.currencyPair = exchangeSettings.currencyPair; - this.tradingReserveAsset = exchangeSettings.tradingReserveAsset; - this.tradingReserveCurrency = exchangeSettings.tradingReserveCurrency; - this.slippagePercentage = exchangeSettings.slippagePercentage; + this.tradingEnabled = tradingEnabled; + this.currencyPair = exchangeSettings.currencyPair; + this.tradingReserveAsset = exchangeSettings.tradingReserveAsset; + this.tradingReserveCurrency = exchangeSettings.tradingReserveCurrency; + this.slippagePercentage = exchangeSettings.slippagePercentage; + this.storage = storage; this.exchangeapi = exchangeapi; this.logger = logger; @@ -31,7 +32,6 @@ agent.prototype.order = function(orderType) { var process = function (err, result) { - //No need to test on error as it's handled by the errorhandler this.calculateOrder(result); if(this.tradingEnabled) { @@ -40,43 +40,80 @@ agent.prototype.order = function(orderType) { this.placeSimulatedOrder(); } - }; + }.bind(this); + + var simulate = function() { + + async.series( + { + balance: function(cb) {cb(null, {assetAvailable: this.simulationBalance.assetAvailable, currencyAvailable: this.simulationBalance.currencyAvailable, fee: this.simulationBalance.fee});}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), + lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) + }, + process.bind(this) + ); + + }.bind(this); + + if(this.tradingEnabled) { + + async.series( + { + balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), + lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) + }, + process.bind(this) + ); + + } else { + + if(!this.simulationBalance) { + + this.exchangeapi.getBalance(true, function(err, result) { + + this.simulationBalance = {assetAvailable: result.assetAvailable, currencyAvailable: result.currencyAvailable, fee: result.fee}; - async.series( - { - balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), - orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), - lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) - }, - process.bind(this) - ); + simulate(); + + }.bind(this)); + + } else { + + simulate(); + + } + + } }; agent.prototype.calculateOrder = function(result) { - this.orderDetails.assetBalance = parseFloat(result.balance.assetAvailable); - this.orderDetails.currencyBalance = parseFloat(result.balance.currencyAvailable); + this.orderDetails.assetAvailable = parseFloat(result.balance.assetAvailable); + this.orderDetails.currencyAvailable = parseFloat(result.balance.currencyAvailable); this.orderDetails.transactionFee = parseFloat(result.balance.fee); var orderBook = result.orderBook; var lastClose = result.lastClose; - this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetBalance + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyBalance + ' Trading Fee: ' + this.orderDetails.transactionFee +')'); + this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetAvailable + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyAvailable + ' Trading Fee: ' + this.orderDetails.transactionFee +')'); if(this.orderDetails.orderType === 'buy') { var lowestAsk = lastClose; var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 8); - var balance = (this.orderDetails.currencyBalance - this.tradingReserveCurrency) * (1 - (this.orderDetails.transactionFee / 100)); + var balance = (this.orderDetails.currencyAvailable - this.tradingReserveCurrency) * (1 - (this.orderDetails.transactionFee / 100)); this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); this.orderDetails.price = lowestAskWithSlippage; this.orderDetails.amount = tools.floor(balance / this.orderDetails.price, 8); + this.simulationBalance = {assetAvailable: tools.round(this.orderDetails.assetAvailable + this.orderDetails.amount,8), currencyAvailable: 0, fee: this.orderDetails.transactionFee}; + } else if(this.orderDetails.orderType === 'sell') { var highestBid = lastClose; @@ -86,10 +123,14 @@ agent.prototype.calculateOrder = function(result) { this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); this.orderDetails.price = highestBidWithSlippage; - this.orderDetails.amount = tools.round(this.orderDetails.assetBalance - this.tradingReserveAsset, 8); + this.orderDetails.amount = tools.round(this.orderDetails.assetAvailable - this.tradingReserveAsset, 8); + + this.simulationBalance = {assetAvailable: 0, currencyAvailable: tools.round(this.orderDetails.currencyAvailable + (this.orderDetails.amount * this.orderDetails.price), 8), fee: this.orderDetails.transactionFee}; } + this.orderDetails.simulationBalance = this.simulationBalance; + }; agent.prototype.placeRealOrder = function() { @@ -116,7 +157,7 @@ agent.prototype.placeSimulatedOrder = function() { this.orderDetails.order = 'Simulated'; - this.orderDetails.status = 'open'; + this.orderDetails.status = 'filled'; this.logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); From aed176e601b5bd0e23d49e6b06373f098b473c2a Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Fri, 6 Nov 2015 23:49:16 +0100 Subject: [PATCH 46/57] Fixes #7 (Bitstamp place order error: Ensure that there are no more than 2 decimal places) --- exchanges/bitstamp.js | 2 ++ services/exchangeapi.js | 2 ++ services/tradingagent.js | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 6788a48..783e097 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -8,6 +8,8 @@ var exchange = function(currencyPair, apiSettings, logger) { this.bitstamp = new Bitstamp(apiSettings.apiKey, apiSettings.secret, apiSettings.clientId); + this.orderPriceMaxDecimals = this.bitstamp.orderPriceMaxDecimals || 2; // with fallback until module bitstamp api is updated + this.q = async.queue(function (task, callback) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); this.logger.debug('There are currently ' + this.q.running() + ' running jobs and ' + this.q.length() + ' jobs in queue.'); diff --git a/services/exchangeapi.js b/services/exchangeapi.js index 1dd4bd7..759de4a 100644 --- a/services/exchangeapi.js +++ b/services/exchangeapi.js @@ -28,6 +28,8 @@ var api = function(exchangeSettings, apiSettings, logger) { } + this.orderPriceMaxDecimals = this.selectedExchange.orderPriceMaxDecimals || 8; + _.bindAll(this, 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); }; diff --git a/services/tradingagent.js b/services/tradingagent.js index 299543d..ebd16d5 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -109,7 +109,7 @@ agent.prototype.calculateOrder = function(result) { this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); - this.orderDetails.price = lowestAskWithSlippage; + this.orderDetails.price = tools.round(lowestAskWithSlippage, this.exchangeapi.orderPriceMaxDecimals); this.orderDetails.amount = tools.floor(balance / this.orderDetails.price, 8); this.simulationBalance = {assetAvailable: tools.round(this.orderDetails.assetAvailable + this.orderDetails.amount,8), currencyAvailable: 0, fee: this.orderDetails.transactionFee}; @@ -122,7 +122,7 @@ agent.prototype.calculateOrder = function(result) { this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); - this.orderDetails.price = highestBidWithSlippage; + this.orderDetails.price = tools.round(highestBidWithSlippage, this.exchangeapi.orderPriceMaxDecimals); this.orderDetails.amount = tools.round(this.orderDetails.assetAvailable - this.tradingReserveAsset, 8); this.simulationBalance = {assetAvailable: 0, currencyAvailable: tools.round(this.orderDetails.currencyAvailable + (this.orderDetails.amount * this.orderDetails.price), 8), fee: this.orderDetails.transactionFee}; @@ -147,7 +147,7 @@ agent.prototype.placeRealOrder = function() { }; -agent.prototype.placeSimulatedOrder = function() { +agent.prototype.placeSimulatedOrder = function() {console.dir(this.orderDetails) if(this.orderDetails.amount <= 0) { From a1aad8038f2d0d7d3842a466593691139ea9daf8 Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Fri, 6 Nov 2015 23:55:39 +0100 Subject: [PATCH 47/57] Remove a console.log --- services/tradingagent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/tradingagent.js b/services/tradingagent.js index ebd16d5..3d9fd0a 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -147,7 +147,7 @@ agent.prototype.placeRealOrder = function() { }; -agent.prototype.placeSimulatedOrder = function() {console.dir(this.orderDetails) +agent.prototype.placeSimulatedOrder = function() { if(this.orderDetails.amount <= 0) { From 8c851a7cd79f6f9b8fea8910c0b39a2778d39f04 Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Sat, 7 Nov 2015 00:29:12 +0100 Subject: [PATCH 48/57] Implement minimum order size (price) --- exchanges/bitstamp.js | 1 + services/exchangeapi.js | 1 + services/tradingagent.js | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index 783e097..23cba38 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -9,6 +9,7 @@ var exchange = function(currencyPair, apiSettings, logger) { this.bitstamp = new Bitstamp(apiSettings.apiKey, apiSettings.secret, apiSettings.clientId); this.orderPriceMaxDecimals = this.bitstamp.orderPriceMaxDecimals || 2; // with fallback until module bitstamp api is updated + this.minimumOrderSize = this.bitstamp.minimumOrderSize || 5; // with fallback until module bitstamp api is updated this.q = async.queue(function (task, callback) { this.logger.debug('Added ' + task.name + ' API call to the queue.'); diff --git a/services/exchangeapi.js b/services/exchangeapi.js index 759de4a..d9a4ced 100644 --- a/services/exchangeapi.js +++ b/services/exchangeapi.js @@ -29,6 +29,7 @@ var api = function(exchangeSettings, apiSettings, logger) { } this.orderPriceMaxDecimals = this.selectedExchange.orderPriceMaxDecimals || 8; + this.minimumOrderSize = this.selectedExchange.minimumOrderSize || 0; _.bindAll(this, 'getTrades', 'getBalance', 'getOrderBook', 'placeOrder', 'orderFilled' ,'cancelOrder'); diff --git a/services/tradingagent.js b/services/tradingagent.js index 3d9fd0a..65d68c5 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -135,7 +135,7 @@ agent.prototype.calculateOrder = function(result) { agent.prototype.placeRealOrder = function() { - if(this.orderDetails.amount <= 0) { + if(this.orderDetails.amount === 0 || this.orderDetails.amount <= this.exchangeapi.minimumOrderSize) { this.logger.log('Insufficient funds to place an order.'); @@ -149,7 +149,7 @@ agent.prototype.placeRealOrder = function() { agent.prototype.placeSimulatedOrder = function() { - if(this.orderDetails.amount <= 0) { + if(this.orderDetails.amount === 0 || this.orderDetails.amount <= this.exchangeapi.minimumOrderSize) { this.logger.log('Insufficient funds to place an order.'); From c92f53b22595a287e994e9f728b199e8a4f2320b Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Sat, 7 Nov 2015 11:01:11 +0100 Subject: [PATCH 49/57] Correct usage of minimum order size --- services/tradingagent.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/services/tradingagent.js b/services/tradingagent.js index 65d68c5..e2811c3 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -32,7 +32,9 @@ agent.prototype.order = function(orderType) { var process = function (err, result) { - this.calculateOrder(result); + if(!this.calculateOrder(result)) { + return false; + } if(this.tradingEnabled) { this.placeRealOrder(); @@ -129,13 +131,20 @@ agent.prototype.calculateOrder = function(result) { } + if (this.orderDetails.amount * this.orderDetails.price < this.exchangeapi.minimumOrderSize) { + this.logger.log('Tradeable amount is below exchange\'s minimum order size.'); + return false; + } + this.orderDetails.simulationBalance = this.simulationBalance; + return true; + }; agent.prototype.placeRealOrder = function() { - if(this.orderDetails.amount === 0 || this.orderDetails.amount <= this.exchangeapi.minimumOrderSize) { + if(this.orderDetails.amount <= 0) { this.logger.log('Insufficient funds to place an order.'); @@ -149,7 +158,7 @@ agent.prototype.placeRealOrder = function() { agent.prototype.placeSimulatedOrder = function() { - if(this.orderDetails.amount === 0 || this.orderDetails.amount <= this.exchangeapi.minimumOrderSize) { + if(this.orderDetails.amount <= 0) { this.logger.log('Insufficient funds to place an order.'); From 03615dad625161af5bc6bc475414b4d6d7d0a1c7 Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Sat, 7 Nov 2015 11:09:24 +0100 Subject: [PATCH 50/57] Add a comment about the logged messages --- services/tradingagent.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/tradingagent.js b/services/tradingagent.js index e2811c3..cff3acb 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -132,6 +132,7 @@ agent.prototype.calculateOrder = function(result) { } if (this.orderDetails.amount * this.orderDetails.price < this.exchangeapi.minimumOrderSize) { + // Could use some refactoring so that we don't log the two previous messages if we can't make an order anyway this.logger.log('Tradeable amount is below exchange\'s minimum order size.'); return false; } From 06d199d55603eeff60ad1b1db7317aef442ce1c9 Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Wed, 11 Nov 2015 16:28:59 +0100 Subject: [PATCH 51/57] Add option to not trade at startup advice --- app.js | 1 + apps/trader.js | 8 +++++++- config.sample.js | 3 +++ services/tradingadvisor.js | 11 +++++++---- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 5ef90ac..5512ecf 100644 --- a/app.js +++ b/app.js @@ -25,6 +25,7 @@ app.prototype.launchTrader = function() { this.logger.log('----------------------------------------------------'); this.logger.log('Launching trader module.'); + this.logger.log('Trade at start = ' + config.tradeAtStart); this.logger.log('----------------------------------------------------'); this.app = require('./apps/trader.js'); this.appListener(); diff --git a/apps/trader.js b/apps/trader.js index 09ba7ce..fcf502b 100644 --- a/apps/trader.js +++ b/apps/trader.js @@ -71,7 +71,13 @@ var trader = function() { advisor.on('advice', function(result) { - this.logger.log('Advice: ' + result.advice + ' (' + result.indicatorValue + ')'); + var indicatorValueStr = result.indicatorValue ? ' (' + result.indicatorValue + ')' : ''; + var adviceStr = result.isStart ? 'Start advice: ' : 'Advice: '; + this.logger.log(adviceStr + result.advice + indicatorValueStr); + + if (!config.tradeAtStart && result.isStart) { + return false; + } if(result.advice === 'buy') { diff --git a/config.sample.js b/config.sample.js index 9cd448f..96c7116 100644 --- a/config.sample.js +++ b/config.sample.js @@ -4,6 +4,9 @@ var config = {}; //------------------------------EnableRealTrading config.tradingEnabled = false; +// If false trading is simulated, candles are still aggregated +config.tradeAtStart = false; +// If true a trade is made immediately if the advice is either buy or sell //------------------------------EnableRealTrading //------------------------------exchangeSettings diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index ac08fc3..9fb4f5d 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -43,7 +43,10 @@ advisor.prototype.start = function(callback) { this.storage.getLastNCompleteAggregatedCandleSticks(1000, this.candleStickSize, function(err, candleSticks) { - this.latestTradeAdvice = {advice: 'hold'}; + this.latestTradeAdvice = { + advice: 'hold', + isStart: true + }; for(var i = 0; i < candleSticks.length; i++) { @@ -51,11 +54,11 @@ advisor.prototype.start = function(callback) { if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { if(['buy', 'sell'].indexOf(result.advice) >= 0) { - this.latestTradeAdvice = result; + _.extend(this.latestTradeAdvice, result); } } else { - var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); - this.logger.error(err.stack); + var _err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); + this.logger.error(_err.stack); process.exit(); } From f6fb129e25e01d262fc3f203d58bd14fc43cc553 Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Tue, 10 Nov 2015 19:18:42 +0100 Subject: [PATCH 52/57] Add .editorconfig - codifies current indenting inconsistencies in the project --- .editorconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a77f8df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[config-sample.js, candleaggregator.js, storage.js, tradingagent.js, tradingadvisor.js] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false From 3187c810ec4d3bea0ee7cb4072ebf6aaf016f194 Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Sun, 15 Nov 2015 22:27:31 +0100 Subject: [PATCH 53/57] Change indent style to 2 spaces. Closes #10 --- .editorconfig | 3 - config.sample.js | 54 ++--- services/candleaggregator.js | 52 ++--- services/storage.js | 418 +++++++++++++++++------------------ services/tradingadvisor.js | 100 ++++----- services/tradingagent.js | 200 ++++++++--------- 6 files changed, 412 insertions(+), 415 deletions(-) diff --git a/.editorconfig b/.editorconfig index a77f8df..4a7ea30 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,8 +8,5 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[config-sample.js, candleaggregator.js, storage.js, tradingagent.js, tradingadvisor.js] -indent_style = tab - [*.md] trim_trailing_whitespace = false diff --git a/config.sample.js b/config.sample.js index 9cd448f..884fdce 100644 --- a/config.sample.js +++ b/config.sample.js @@ -8,28 +8,28 @@ config.tradingEnabled = false; //------------------------------exchangeSettings config.exchangeSettings = { - exchange: '', - // Options: (bitstamp, kraken, btce) - currencyPair: {pair: '', asset: '', currency: ''}, - // For Bitstamp just use {pair: 'XBTUSD', asset: 'XBT', currency: 'USD'} - // For Kraken look up the currency pairs in their API: https://api.kraken.com/0/public/AssetPairs - // Kraken Example: {pair: 'XXBTZEUR', asset: 'XXBT', currency: 'ZEUR'} - // For BTC-E look up the currency pairs in their API: https://btc-e.com/api/3/info - // BTC-E Example: {pair: 'BTC_USD', asset: 'BTC', currency: 'USD'} - tradingReserveAsset: 0, - // Enter an amount of "asset" you would like to freeze (not trade). - tradingReserveCurrency: 0, - // Enter an amount of "currency" you would like to freeze (not trade). - slippagePercentage: 0.1 - // Percentage to sell below and buy above the market. + exchange: '', + // Options: (bitstamp, kraken, btce) + currencyPair: {pair: '', asset: '', currency: ''}, + // For Bitstamp just use {pair: 'XBTUSD', asset: 'XBT', currency: 'USD'} + // For Kraken look up the currency pairs in their API: https://api.kraken.com/0/public/AssetPairs + // Kraken Example: {pair: 'XXBTZEUR', asset: 'XXBT', currency: 'ZEUR'} + // For BTC-E look up the currency pairs in their API: https://btc-e.com/api/3/info + // BTC-E Example: {pair: 'BTC_USD', asset: 'BTC', currency: 'USD'} + tradingReserveAsset: 0, + // Enter an amount of "asset" you would like to freeze (not trade). + tradingReserveCurrency: 0, + // Enter an amount of "currency" you would like to freeze (not trade). + slippagePercentage: 0.1 + // Percentage to sell below and buy above the market. }; //------------------------------exchangeSettings //------------------------------APISettings config.apiSettings = { - bitstamp: {clientId: 0, apiKey: '', secret: ''}, - kraken: {apiKey: '', secret: ''}, - btce: {apiKey: '', secret: ''} + bitstamp: {clientId: 0, apiKey: '', secret: ''}, + kraken: {apiKey: '', secret: ''}, + btce: {apiKey: '', secret: ''} }; //------------------------------APISettings @@ -46,11 +46,11 @@ config.downloaderRefreshSeconds = 10; //------------------------------IndicatorSettings config.indicatorSettings = { - indicator: 'MACD', - // Choices: Any indicator from the indicators folder. - options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyThreshold: 0, sellThreshold: 0}, - // Options needed for your indicator (Look them up in the indicator's file). - candleStickSizeMinutes: 5 + indicator: 'MACD', + // Choices: Any indicator from the indicators folder. + options: {neededPeriods: 26, shortPeriods: 12, longPeriods: 26, emaPeriods: 9, buyThreshold: 0, sellThreshold: 0}, + // Options needed for your indicator (Look them up in the indicator's file). + candleStickSizeMinutes: 5 }; //------------------------------IndicatorSettings @@ -60,17 +60,17 @@ config.orderKeepAliveMinutes = config.indicatorSettings.candleStickSizeMinutes / //------------------------------PushOver config.pushOver = { - enabled: false, - pushUserId: '', - pushAppToken: '' + enabled: false, + pushUserId: '', + pushAppToken: '' }; // Push notifications via pushover (https://pushover.net/). //------------------------------PushOver //------------------------------BackTesting config.backTesterSettings = { - initialAssetBalance: 0, - initialCurrencyBalance: 10000 + initialAssetBalance: 0, + initialCurrencyBalance: 10000 }; //------------------------------BackTesting diff --git a/services/candleaggregator.js b/services/candleaggregator.js index 6aba9af..5adabb7 100644 --- a/services/candleaggregator.js +++ b/services/candleaggregator.js @@ -3,12 +3,12 @@ var tools = require('../util/tools.js'); var aggregator = function(candleStickSizeMinutes, storage, logger) { - this.storage = storage; - this.candleStickSizeMinutes = candleStickSizeMinutes; - this.logger = logger; - this.previousCompleteCandleStickPeriod = 0; + this.storage = storage; + this.candleStickSizeMinutes = candleStickSizeMinutes; + this.logger = logger; + this.previousCompleteCandleStickPeriod = 0; - _.bindAll(this, 'update', 'setCandleStickSize'); + _.bindAll(this, 'update', 'setCandleStickSize'); }; @@ -20,50 +20,50 @@ Util.inherits(aggregator, EventEmitter); aggregator.prototype.update = function() { - this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSizeMinutes, function(err, completeCandleStick) { + this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSizeMinutes, function(err, completeCandleStick) { - if(completeCandleStick) { + if(completeCandleStick) { - if(this.previousCompleteCandleStickPeriod === 0) { + if(this.previousCompleteCandleStickPeriod === 0) { - this.previousCompleteCandleStickPeriod = completeCandleStick.period; + this.previousCompleteCandleStickPeriod = completeCandleStick.period; - } + } - if(completeCandleStick.period !== this.previousCompleteCandleStickPeriod) { + if(completeCandleStick.period !== this.previousCompleteCandleStickPeriod) { - this.logger.log('Created a new ' + this.candleStickSizeMinutes + ' minute candlestick!'); - this.logger.log(JSON.stringify(completeCandleStick)); + this.logger.log('Created a new ' + this.candleStickSizeMinutes + ' minute candlestick!'); + this.logger.log(JSON.stringify(completeCandleStick)); - this.previousCompleteCandleStickPeriod = completeCandleStick.period; + this.previousCompleteCandleStickPeriod = completeCandleStick.period; - this.storage.removeOldDBCandles(this.candleStickSizeMinutes, function(err) { + this.storage.removeOldDBCandles(this.candleStickSizeMinutes, function(err) { - this.emit('update', completeCandleStick); + this.emit('update', completeCandleStick); - }.bind(this)); + }.bind(this)); - } + } - } + } - }.bind(this)); + }.bind(this)); }; aggregator.prototype.setCandleStickSize = function(candleStickSizeMinutes) { - this.candleStickSizeMinutes = candleStickSizeMinutes; + this.candleStickSizeMinutes = candleStickSizeMinutes; - this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSizeMinutes, function(err, completeCandleStick) { + this.storage.getLastCompleteAggregatedCandleStick(this.candleStickSizeMinutes, function(err, completeCandleStick) { - if(completeCandleStick) { + if(completeCandleStick) { - this.previousCompleteCandleStickPeriod = completeCandleStick.period; + this.previousCompleteCandleStickPeriod = completeCandleStick.period; - } + } - }.bind(this)); + }.bind(this)); }; diff --git a/services/storage.js b/services/storage.js index f92c34c..dd1ea81 100644 --- a/services/storage.js +++ b/services/storage.js @@ -5,445 +5,445 @@ var tools = require('../util/tools.js'); var storage = function(exchangeSettings, mongoConnectionString, logger) { - this.pair = exchangeSettings.currencyPair.pair; - this.exchange = exchangeSettings.exchange; - this.exchangePair = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; - this.mongoConnectionString = mongoConnectionString; - this.logger = logger; + this.pair = exchangeSettings.currencyPair.pair; + this.exchange = exchangeSettings.exchange; + this.exchangePair = exchangeSettings.exchange + exchangeSettings.currencyPair.pair; + this.mongoConnectionString = mongoConnectionString; + this.logger = logger; - _.bindAll(this, 'push', 'getLastNCandles', 'getAllCandles', 'getAllCandlesSince', 'getLastClose', 'getLastNonEmptyPeriod', 'getLastNonEmptyClose', 'getLastNCompleteAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getCompleteAggregatedCandleSticks', 'getLastNAggregatedCandleSticks', 'getAggregatedCandleSticks', 'getAggregatedCandleSticksSince', 'calculateAggregatedCandleStick', 'aggregateCandleSticks', 'removeOldDBCandles', 'dropCollection', 'getInitialBalance', 'setInitialBalance'); + _.bindAll(this, 'push', 'getLastNCandles', 'getAllCandles', 'getAllCandlesSince', 'getLastClose', 'getLastNonEmptyPeriod', 'getLastNonEmptyClose', 'getLastNCompleteAggregatedCandleSticks', 'getLastCompleteAggregatedCandleStick', 'getCompleteAggregatedCandleSticks', 'getLastNAggregatedCandleSticks', 'getAggregatedCandleSticks', 'getAggregatedCandleSticksSince', 'calculateAggregatedCandleStick', 'aggregateCandleSticks', 'removeOldDBCandles', 'dropCollection', 'getInitialBalance', 'setInitialBalance'); }; storage.prototype.push = function(csArray, callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - var bulk = csCollection.initializeOrderedBulkOp(); + var bulk = csCollection.initializeOrderedBulkOp(); - _.forEach(csArray, function(cs) { - bulk.find({period: cs.period}).upsert().updateOne(cs); - }); + _.forEach(csArray, function(cs) { + bulk.find({period: cs.period}).upsert().updateOne(cs); + }); - bulk.execute(function(err, res) { + bulk.execute(function(err, res) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err); + callback(err); - } else { + } else { - callback(null); + callback(null); - } + } - }); + }); }; storage.prototype.getLastNCandles = function(N, callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.find({}).sort({period:-1}).limit(N, function(err, candlesSticks) { + csCollection.find({}).sort({period:-1}).limit(N, function(err, candlesSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err, []); + callback(err, []); - } else { + } else { - callback(null, candlesSticks.reverse()); + callback(null, candlesSticks.reverse()); - } + } - }); + }); }; storage.prototype.getAllCandles = function(callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.find({}).sort({period:1}, function(err, candlesSticks) { + csCollection.find({}).sort({period:1}, function(err, candlesSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err, []); + callback(err, []); - } else { + } else { - callback(null, candlesSticks); + callback(null, candlesSticks); - } + } - }); + }); }; storage.prototype.getAllCandlesSince = function(period, callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.find({period: { $gte: period }}).sort({period:1}, function(err, candlesSticks) { + csCollection.find({period: { $gte: period }}).sort({period:1}, function(err, candlesSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err, []); + callback(err, []); - } else { + } else { - callback(null, candlesSticks); + callback(null, candlesSticks); - } + } - }); + }); }; storage.prototype.getLastClose = function(callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.find({}).sort({period:-1}).limit(1, function(err, candleSticks) { + csCollection.find({}).sort({period:-1}).limit(1, function(err, candleSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err, 0); + callback(err, 0); - } else { + } else { - if(candleSticks.length > 0) { - callback(null, candleSticks[0].close); - } else { - callback(null, 0); - } + if(candleSticks.length > 0) { + callback(null, candleSticks[0].close); + } else { + callback(null, 0); + } - } + } - }); + }); }; storage.prototype.getLastNonEmptyPeriod = function(callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { + csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err, 0); + callback(err, 0); - } else { + } else { - if(candleSticks.length > 0) { - callback(null, candleSticks[0].period); - } else { - callback(null, 0); - } + if(candleSticks.length > 0) { + callback(null, candleSticks[0].period); + } else { + callback(null, 0); + } - } + } - }); + }); }; storage.prototype.getLastNonEmptyClose = function(callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { + csCollection.find({volume: { $gt: 0 }}).sort({period:-1}).limit(1, function(err, candleSticks) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err, 0); + callback(err, 0); - } else { + } else { - if(candleSticks.length > 0) { - callback(null, candleSticks[0].close); - } else { - callback(null, 0); - } + if(candleSticks.length > 0) { + callback(null, candleSticks[0].close); + } else { + callback(null, 0); + } - } + } - }); + }); }; storage.prototype.getLastNCompleteAggregatedCandleSticks = function(N, candleStickSize, callback) { - this.getLastNAggregatedCandleSticks(N + 1, candleStickSize, function(err, aggregatedCandleSticks) { - aggregatedCandleSticks.pop(); - callback(null, aggregatedCandleSticks); - }); + this.getLastNAggregatedCandleSticks(N + 1, candleStickSize, function(err, aggregatedCandleSticks) { + aggregatedCandleSticks.pop(); + callback(null, aggregatedCandleSticks); + }); }; storage.prototype.getLastCompleteAggregatedCandleStick = function(candleStickSize, callback) { - this.getLastNAggregatedCandleSticks(2, candleStickSize, function(err, aggregatedCandleSticks) { - aggregatedCandleSticks.pop(); - callback(null, _.last(aggregatedCandleSticks)); - }); + this.getLastNAggregatedCandleSticks(2, candleStickSize, function(err, aggregatedCandleSticks) { + aggregatedCandleSticks.pop(); + callback(null, _.last(aggregatedCandleSticks)); + }); }; storage.prototype.getCompleteAggregatedCandleSticks = function(candleStickSize, callback) { - this.getAggregatedCandleSticks(candleStickSize, function(err, aggregatedCandleSticks) { - aggregatedCandleSticks.pop(); - callback(null, aggregatedCandleSticks); - }); + this.getAggregatedCandleSticks(candleStickSize, function(err, aggregatedCandleSticks) { + aggregatedCandleSticks.pop(); + callback(null, aggregatedCandleSticks); + }); }; storage.prototype.getLastNAggregatedCandleSticks = function(N, candleStickSize, callback) { - var candleStickSizeSeconds = candleStickSize * 60; + var candleStickSizeSeconds = candleStickSize * 60; - var now = tools.unixTimeStamp(new Date().getTime()); - var closestCandleStick = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); + var now = tools.unixTimeStamp(new Date().getTime()); + var closestCandleStick = (Math.floor(now/candleStickSizeSeconds)*candleStickSizeSeconds); - var startRange = closestCandleStick - (candleStickSizeSeconds * N); + var startRange = closestCandleStick - (candleStickSizeSeconds * N); - this.getAllCandlesSince(startRange, function(err, candleSticks) { + this.getAllCandlesSince(startRange, function(err, candleSticks) { - if(candleSticks.length > 0) { + if(candleSticks.length > 0) { - var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); + var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); - callback(null, aggregatedCandleSticks); + callback(null, aggregatedCandleSticks); - } else { + } else { - callback(null, []); + callback(null, []); - } + } - }.bind(this)); + }.bind(this)); }; storage.prototype.getAggregatedCandleSticks = function(candleStickSize, callback) { - this.getAllCandlesSince(0, function(err, candleSticks) { + this.getAllCandlesSince(0, function(err, candleSticks) { - if(candleSticks.length > 0) { + if(candleSticks.length > 0) { - var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); + var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); - callback(null, aggregatedCandleSticks); + callback(null, aggregatedCandleSticks); - } else { + } else { - callback(null, []); + callback(null, []); - } + } - }.bind(this)); + }.bind(this)); }; storage.prototype.getAggregatedCandleSticksSince = function(candleStickSize, period, callback) { - this.getAllCandlesSince(period, function(err, candleSticks) { + this.getAllCandlesSince(period, function(err, candleSticks) { - if(candleSticks.length > 0) { + if(candleSticks.length > 0) { - var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); + var aggregatedCandleSticks = this.aggregateCandleSticks(candleStickSize, candleSticks); - callback(null, aggregatedCandleSticks); + callback(null, aggregatedCandleSticks); - } else { + } else { - callback(null, []); + callback(null, []); - } + } - }.bind(this)); + }.bind(this)); }; storage.prototype.calculateAggregatedCandleStick = function(period, relevantSticks) { - var currentCandleStick = {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; + var currentCandleStick = {'period':period,'open':undefined,'high':undefined,'low':undefined,'close':undefined,'volume':0, 'vwap':undefined}; - currentCandleStick.open = relevantSticks[0].open; - currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; - currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; - currentCandleStick.close = relevantSticks[relevantSticks.length - 1].close; - currentCandleStick.volume = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + entry.volume; }, 0), 8); - if(currentCandleStick.volume === 0) { - currentCandleStick.vwap = currentCandleStick.close; - } else { - currentCandleStick.vwap = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + (entry.vwap * entry.volume); }, 0) / currentCandleStick.volume, 8); - } + currentCandleStick.open = relevantSticks[0].open; + currentCandleStick.high = _.max(relevantSticks, function(relevantStick) { return relevantStick.high; }).high; + currentCandleStick.low = _.min(relevantSticks, function(relevantStick) { return relevantStick.low; }).low; + currentCandleStick.close = relevantSticks[relevantSticks.length - 1].close; + currentCandleStick.volume = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + entry.volume; }, 0), 8); + if(currentCandleStick.volume === 0) { + currentCandleStick.vwap = currentCandleStick.close; + } else { + currentCandleStick.vwap = tools.round(_.reduce(relevantSticks, function(memo, entry) { return memo + (entry.vwap * entry.volume); }, 0) / currentCandleStick.volume, 8); + } - return currentCandleStick; + return currentCandleStick; }; storage.prototype.aggregateCandleSticks = function(candleStickSize, candleSticks) { - var candleStickSizeSeconds = 60 * candleStickSize; + var candleStickSizeSeconds = 60 * candleStickSize; - var aggregatedCandleSticks = []; + var aggregatedCandleSticks = []; - var startTimeStamp = Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds; - var beginPeriod = startTimeStamp; - var endPeriod = startTimeStamp + candleStickSizeSeconds; - var stopTimeStamp = _.last(candleSticks).period; + var startTimeStamp = Math.floor(candleSticks[0].period / candleStickSizeSeconds) * candleStickSizeSeconds; + var beginPeriod = startTimeStamp; + var endPeriod = startTimeStamp + candleStickSizeSeconds; + var stopTimeStamp = _.last(candleSticks).period; - var relevantSticks = []; + var relevantSticks = []; - var filterOnVolume = function(candleStick) { return candleStick.volume > 0; }; + var filterOnVolume = function(candleStick) { return candleStick.volume > 0; }; - _.each(candleSticks, function(candleStick) { + _.each(candleSticks, function(candleStick) { - if(candleStick.period >= beginPeriod && candleStick.period < endPeriod) { + if(candleStick.period >= beginPeriod && candleStick.period < endPeriod) { - relevantSticks.push(candleStick); + relevantSticks.push(candleStick); - } else { + } else { - var vrelevantSticks = _.filter(relevantSticks, filterOnVolume); + var vrelevantSticks = _.filter(relevantSticks, filterOnVolume); - if(vrelevantSticks.length > 0) { - relevantSticks = vrelevantSticks; - } + if(vrelevantSticks.length > 0) { + relevantSticks = vrelevantSticks; + } - aggregatedCandleSticks.push(this.calculateAggregatedCandleStick(beginPeriod, relevantSticks)); + aggregatedCandleSticks.push(this.calculateAggregatedCandleStick(beginPeriod, relevantSticks)); - beginPeriod = endPeriod; - endPeriod = endPeriod + candleStickSizeSeconds; + beginPeriod = endPeriod; + endPeriod = endPeriod + candleStickSizeSeconds; - relevantSticks = []; + relevantSticks = []; - relevantSticks.push(candleStick); + relevantSticks.push(candleStick); - } + } - }.bind(this)); + }.bind(this)); - if(relevantSticks.length > 0) { - aggregatedCandleSticks.push(this.calculateAggregatedCandleStick(beginPeriod, relevantSticks)); - } + if(relevantSticks.length > 0) { + aggregatedCandleSticks.push(this.calculateAggregatedCandleStick(beginPeriod, relevantSticks)); + } - return aggregatedCandleSticks; + return aggregatedCandleSticks; }; storage.prototype.removeOldDBCandles = function(candleStickSize, callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(this.exchangePair); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(this.exchangePair); - var candleStickSizeSeconds = candleStickSize * 60; + var candleStickSizeSeconds = candleStickSize * 60; - var now = Math.floor(tools.unixTimeStamp(new Date().getTime()) / candleStickSizeSeconds) * candleStickSizeSeconds; - var oldPeriod = now - (candleStickSizeSeconds * 10000); + var now = Math.floor(tools.unixTimeStamp(new Date().getTime()) / candleStickSizeSeconds) * candleStickSizeSeconds; + var oldPeriod = now - (candleStickSizeSeconds * 10000); - csCollection.remove({ period: { $lt: oldPeriod } }, function(err, resp) { + csCollection.remove({ period: { $lt: oldPeriod } }, function(err, resp) { - csDatastore.close(); + csDatastore.close(); - callback(null); + callback(null); - }); + }); }; storage.prototype.dropCollection = function(collection, callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection(collection); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection(collection); - csCollection.drop(function(err) { + csCollection.drop(function(err) { - csDatastore.close(); + csDatastore.close(); - callback(err); + callback(err); - }); + }); }; storage.prototype.setInitialBalance = function(initialBalance, callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection('balance'); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); - csCollection.update({exchangePair: this.exchangePair}, {exchangePair: this.exchangePair, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { + csCollection.update({exchangePair: this.exchangePair}, {exchangePair: this.exchangePair, initialBalance: initialBalance}, { upsert: true }, function(err, doc) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err); + callback(err); - } else { + } else { - callback(null); + callback(null); - } + } - }.bind(this)); + }.bind(this)); }; storage.prototype.getInitialBalance = function(callback) { - var csDatastore = mongo(this.mongoConnectionString); - var csCollection = csDatastore.collection('balance'); + var csDatastore = mongo(this.mongoConnectionString); + var csCollection = csDatastore.collection('balance'); - csCollection.find({exchangePair: this.exchangePair}).limit(1, function(err, doc) { + csCollection.find({exchangePair: this.exchangePair}).limit(1, function(err, doc) { - csDatastore.close(); + csDatastore.close(); - if(err) { + if(err) { - callback(err); + callback(err); - } else if(doc.length > 0 ){ + } else if(doc.length > 0 ){ - var initialBalance = doc[0].initialBalance; + var initialBalance = doc[0].initialBalance; - callback(null, initialBalance); + callback(null, initialBalance); - } else { + } else { - callback(null, null); + callback(null, null); - } + } - }.bind(this)); + }.bind(this)); }; diff --git a/services/tradingadvisor.js b/services/tradingadvisor.js index ac08fc3..7fccd2f 100644 --- a/services/tradingadvisor.js +++ b/services/tradingadvisor.js @@ -4,32 +4,32 @@ var fs = require('fs'); var advisor = function(indicatorSettings, storage, logger) { - this.candleStickSize = indicatorSettings.candleStickSizeMinutes; - this.storage = storage; - this.logger = logger; + this.candleStickSize = indicatorSettings.candleStickSizeMinutes; + this.storage = storage; + this.logger = logger; - try { + try { - this.indicators = {}; + this.indicators = {}; - fs.readdirSync('./indicators/').forEach(function(file) { - if(file != 'template.js' && file.indexOf('.') > 0 && file.indexOf('.js') > 0) { - var indicator = require('../indicators/' + file); - this.indicators[file.replace('.js', '')] = indicator; - } - }.bind(this)); + fs.readdirSync('./indicators/').forEach(function(file) { + if(file != 'template.js' && file.indexOf('.') > 0 && file.indexOf('.js') > 0) { + var indicator = require('../indicators/' + file); + this.indicators[file.replace('.js', '')] = indicator; + } + }.bind(this)); - this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); + this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); - } catch(err) { + } catch(err) { - this.logger.error('Wrong indicator chosen. This indicator doesn\'t exist.'); - this.logger.error(err.stack); - process.exit(); + this.logger.error('Wrong indicator chosen. This indicator doesn\'t exist.'); + this.logger.error(err.stack); + process.exit(); - } + } - _.bindAll(this, 'start', 'update', 'setPosition', 'setIndicator'); + _.bindAll(this, 'start', 'update', 'setPosition', 'setIndicator'); }; @@ -41,62 +41,62 @@ Util.inherits(advisor, EventEmitter); advisor.prototype.start = function(callback) { - this.storage.getLastNCompleteAggregatedCandleSticks(1000, this.candleStickSize, function(err, candleSticks) { + this.storage.getLastNCompleteAggregatedCandleSticks(1000, this.candleStickSize, function(err, candleSticks) { - this.latestTradeAdvice = {advice: 'hold'}; + this.latestTradeAdvice = {advice: 'hold'}; - for(var i = 0; i < candleSticks.length; i++) { + for(var i = 0; i < candleSticks.length; i++) { - var result = this.selectedIndicator.calculate(candleSticks[i]); + var result = this.selectedIndicator.calculate(candleSticks[i]); - if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { - if(['buy', 'sell'].indexOf(result.advice) >= 0) { - this.latestTradeAdvice = result; - } - } else { - var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); - this.logger.error(err.stack); - process.exit(); - } + if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { + if(['buy', 'sell'].indexOf(result.advice) >= 0) { + this.latestTradeAdvice = result; + } + } else { + var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); + this.logger.error(err.stack); + process.exit(); + } - } + } - if(['buy', 'sell'].indexOf(this.latestTradeAdvice.advice) >= 0) { - this.emit('advice', this.latestTradeAdvice); - } + if(['buy', 'sell'].indexOf(this.latestTradeAdvice.advice) >= 0) { + this.emit('advice', this.latestTradeAdvice); + } - if(callback) { - callback(); - } + if(callback) { + callback(); + } - }.bind(this)); + }.bind(this)); }; advisor.prototype.update = function(cs) { - var result = this.selectedIndicator.calculate(cs); + var result = this.selectedIndicator.calculate(cs); - if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { - this.emit('advice', result); - return result; - } else { - var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); - this.logger.error(err.stack); - process.exit(); - } + if(['buy', 'sell', 'hold'].indexOf(result.advice) >= 0) { + this.emit('advice', result); + return result; + } else { + var err = new Error('Invalid advice from indicator, should be either: buy, sell or hold.'); + this.logger.error(err.stack); + process.exit(); + } }; advisor.prototype.setPosition = function(pos) { - this.selectedIndicator.setPosition(pos); + this.selectedIndicator.setPosition(pos); }; advisor.prototype.setIndicator = function(indicatorSettings) { - this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); + this.selectedIndicator = new this.indicators[indicatorSettings.indicator](indicatorSettings.options); }; diff --git a/services/tradingagent.js b/services/tradingagent.js index cff3acb..5517842 100644 --- a/services/tradingagent.js +++ b/services/tradingagent.js @@ -4,17 +4,17 @@ var async = require('async'); var agent = function(tradingEnabled, exchangeSettings, storage, exchangeapi, logger) { - _.bindAll(this, 'order', 'calculateOrder', 'placeRealOrder', 'placeSimulatedOrder', 'processOrder'); + _.bindAll(this, 'order', 'calculateOrder', 'placeRealOrder', 'placeSimulatedOrder', 'processOrder'); - this.tradingEnabled = tradingEnabled; - this.currencyPair = exchangeSettings.currencyPair; - this.tradingReserveAsset = exchangeSettings.tradingReserveAsset; - this.tradingReserveCurrency = exchangeSettings.tradingReserveCurrency; - this.slippagePercentage = exchangeSettings.slippagePercentage; + this.tradingEnabled = tradingEnabled; + this.currencyPair = exchangeSettings.currencyPair; + this.tradingReserveAsset = exchangeSettings.tradingReserveAsset; + this.tradingReserveCurrency = exchangeSettings.tradingReserveCurrency; + this.slippagePercentage = exchangeSettings.slippagePercentage; - this.storage = storage; - this.exchangeapi = exchangeapi; - this.logger = logger; + this.storage = storage; + this.exchangeapi = exchangeapi; + this.logger = logger; }; @@ -26,174 +26,174 @@ Util.inherits(agent, EventEmitter); agent.prototype.order = function(orderType) { - this.orderDetails = {}; + this.orderDetails = {}; - this.orderDetails.orderType = orderType; + this.orderDetails.orderType = orderType; - var process = function (err, result) { + var process = function (err, result) { - if(!this.calculateOrder(result)) { - return false; - } + if(!this.calculateOrder(result)) { + return false; + } - if(this.tradingEnabled) { - this.placeRealOrder(); - } else { - this.placeSimulatedOrder(); - } + if(this.tradingEnabled) { + this.placeRealOrder(); + } else { + this.placeSimulatedOrder(); + } - }.bind(this); + }.bind(this); - var simulate = function() { + var simulate = function() { - async.series( - { - balance: function(cb) {cb(null, {assetAvailable: this.simulationBalance.assetAvailable, currencyAvailable: this.simulationBalance.currencyAvailable, fee: this.simulationBalance.fee});}.bind(this), - orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), - lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) - }, - process.bind(this) - ); + async.series( + { + balance: function(cb) {cb(null, {assetAvailable: this.simulationBalance.assetAvailable, currencyAvailable: this.simulationBalance.currencyAvailable, fee: this.simulationBalance.fee});}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), + lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) + }, + process.bind(this) + ); - }.bind(this); + }.bind(this); - if(this.tradingEnabled) { + if(this.tradingEnabled) { - async.series( - { - balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), - orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), - lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) - }, - process.bind(this) - ); + async.series( + { + balance: function(cb) {this.exchangeapi.getBalance(true, cb);}.bind(this), + orderBook: function(cb) {this.exchangeapi.getOrderBook(true, cb);}.bind(this), + lastClose: function(cb) {this.storage.getLastClose(cb);}.bind(this) + }, + process.bind(this) + ); - } else { + } else { - if(!this.simulationBalance) { + if(!this.simulationBalance) { - this.exchangeapi.getBalance(true, function(err, result) { + this.exchangeapi.getBalance(true, function(err, result) { - this.simulationBalance = {assetAvailable: result.assetAvailable, currencyAvailable: result.currencyAvailable, fee: result.fee}; + this.simulationBalance = {assetAvailable: result.assetAvailable, currencyAvailable: result.currencyAvailable, fee: result.fee}; - simulate(); + simulate(); - }.bind(this)); + }.bind(this)); - } else { + } else { - simulate(); + simulate(); - } + } - } + } }; agent.prototype.calculateOrder = function(result) { - this.orderDetails.assetAvailable = parseFloat(result.balance.assetAvailable); - this.orderDetails.currencyAvailable = parseFloat(result.balance.currencyAvailable); - this.orderDetails.transactionFee = parseFloat(result.balance.fee); + this.orderDetails.assetAvailable = parseFloat(result.balance.assetAvailable); + this.orderDetails.currencyAvailable = parseFloat(result.balance.currencyAvailable); + this.orderDetails.transactionFee = parseFloat(result.balance.fee); - var orderBook = result.orderBook; + var orderBook = result.orderBook; - var lastClose = result.lastClose; + var lastClose = result.lastClose; - this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetAvailable + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyAvailable + ' Trading Fee: ' + this.orderDetails.transactionFee +')'); + this.logger.log('Preparing to place a ' + this.orderDetails.orderType + ' order! (' + this.currencyPair.asset + ' Balance: ' + this.orderDetails.assetAvailable + ' ' + this.currencyPair.currency + ' Balance: ' + this.orderDetails.currencyAvailable + ' Trading Fee: ' + this.orderDetails.transactionFee +')'); - if(this.orderDetails.orderType === 'buy') { + if(this.orderDetails.orderType === 'buy') { - var lowestAsk = lastClose; + var lowestAsk = lastClose; - var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 8); - var balance = (this.orderDetails.currencyAvailable - this.tradingReserveCurrency) * (1 - (this.orderDetails.transactionFee / 100)); + var lowestAskWithSlippage = tools.round(lowestAsk * (1 + (this.slippagePercentage / 100)), 8); + var balance = (this.orderDetails.currencyAvailable - this.tradingReserveCurrency) * (1 - (this.orderDetails.transactionFee / 100)); - this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); + this.logger.log('Lowest Ask: ' + lowestAsk + ' Lowest Ask With Slippage: ' + lowestAskWithSlippage); - this.orderDetails.price = tools.round(lowestAskWithSlippage, this.exchangeapi.orderPriceMaxDecimals); - this.orderDetails.amount = tools.floor(balance / this.orderDetails.price, 8); + this.orderDetails.price = tools.round(lowestAskWithSlippage, this.exchangeapi.orderPriceMaxDecimals); + this.orderDetails.amount = tools.floor(balance / this.orderDetails.price, 8); - this.simulationBalance = {assetAvailable: tools.round(this.orderDetails.assetAvailable + this.orderDetails.amount,8), currencyAvailable: 0, fee: this.orderDetails.transactionFee}; + this.simulationBalance = {assetAvailable: tools.round(this.orderDetails.assetAvailable + this.orderDetails.amount,8), currencyAvailable: 0, fee: this.orderDetails.transactionFee}; - } else if(this.orderDetails.orderType === 'sell') { + } else if(this.orderDetails.orderType === 'sell') { - var highestBid = lastClose; + var highestBid = lastClose; - var highestBidWithSlippage = tools.round(highestBid * (1 - (this.slippagePercentage / 100)), 8); + var highestBidWithSlippage = tools.round(highestBid * (1 - (this.slippagePercentage / 100)), 8); - this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); + this.logger.log('Highest Bid: ' + highestBid + ' Highest Bid With Slippage: ' + highestBidWithSlippage); - this.orderDetails.price = tools.round(highestBidWithSlippage, this.exchangeapi.orderPriceMaxDecimals); - this.orderDetails.amount = tools.round(this.orderDetails.assetAvailable - this.tradingReserveAsset, 8); + this.orderDetails.price = tools.round(highestBidWithSlippage, this.exchangeapi.orderPriceMaxDecimals); + this.orderDetails.amount = tools.round(this.orderDetails.assetAvailable - this.tradingReserveAsset, 8); - this.simulationBalance = {assetAvailable: 0, currencyAvailable: tools.round(this.orderDetails.currencyAvailable + (this.orderDetails.amount * this.orderDetails.price), 8), fee: this.orderDetails.transactionFee}; + this.simulationBalance = {assetAvailable: 0, currencyAvailable: tools.round(this.orderDetails.currencyAvailable + (this.orderDetails.amount * this.orderDetails.price), 8), fee: this.orderDetails.transactionFee}; - } + } - if (this.orderDetails.amount * this.orderDetails.price < this.exchangeapi.minimumOrderSize) { - // Could use some refactoring so that we don't log the two previous messages if we can't make an order anyway - this.logger.log('Tradeable amount is below exchange\'s minimum order size.'); - return false; - } + if (this.orderDetails.amount * this.orderDetails.price < this.exchangeapi.minimumOrderSize) { + // Could use some refactoring so that we don't log the two previous messages if we can't make an order anyway + this.logger.log('Tradeable amount is below exchange\'s minimum order size.'); + return false; + } - this.orderDetails.simulationBalance = this.simulationBalance; + this.orderDetails.simulationBalance = this.simulationBalance; - return true; + return true; }; agent.prototype.placeRealOrder = function() { - if(this.orderDetails.amount <= 0) { + if(this.orderDetails.amount <= 0) { - this.logger.log('Insufficient funds to place an order.'); + this.logger.log('Insufficient funds to place an order.'); - } else { + } else { - this.exchangeapi.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, true, this.processOrder); + this.exchangeapi.placeOrder(this.orderDetails.orderType, this.orderDetails.amount, this.orderDetails.price, true, this.processOrder); - } + } }; agent.prototype.placeSimulatedOrder = function() { - if(this.orderDetails.amount <= 0) { + if(this.orderDetails.amount <= 0) { - this.logger.log('Insufficient funds to place an order.'); + this.logger.log('Insufficient funds to place an order.'); - } else { + } else { - this.orderDetails.order = 'Simulated'; + this.orderDetails.order = 'Simulated'; - this.orderDetails.status = 'filled'; + this.orderDetails.status = 'filled'; - this.logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); + this.logger.log('Placed simulated ' + this.orderDetails.orderType + ' order: (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); - this.emit('simulatedOrder', this.orderDetails); + this.emit('simulatedOrder', this.orderDetails); - } + } }; agent.prototype.processOrder = function(err, order) { - if(!order) { + if(!order) { - this.logger.log('Something went wrong when placing the ' + this.orderDetails.orderType + ' order.'); + this.logger.log('Something went wrong when placing the ' + this.orderDetails.orderType + ' order.'); - } else { + } else { - this.orderDetails.order = order.txid; + this.orderDetails.order = order.txid; - this.orderDetails.status = order.status; + this.orderDetails.status = order.status; - this.logger.log('Placed ' + this.orderDetails.orderType + ' order: ' + this.orderDetails.order + ' (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); + this.logger.log('Placed ' + this.orderDetails.orderType + ' order: ' + this.orderDetails.order + ' (' + this.orderDetails.amount + '@' + this.orderDetails.price + ')'); - this.emit('realOrder', this.orderDetails); + this.emit('realOrder', this.orderDetails); - } + } }; From 556e765da226f333391f0be7ebe6724f82ea712a Mon Sep 17 00:00:00 2001 From: Marc Diethelm Date: Sun, 13 Mar 2016 12:20:56 +0100 Subject: [PATCH 54/57] Make maximum failure count of data retrieval configurable and automatic quitting preventable --- apps/trader.js | 2 +- config.sample.js | 2 ++ services/dataretriever.js | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/trader.js b/apps/trader.js index fcf502b..05e4d77 100644 --- a/apps/trader.js +++ b/apps/trader.js @@ -21,7 +21,7 @@ var config = require('../config.js'); var logger = new loggingservice('trader', config.debug); var storage = new storageservice(config.exchangeSettings, config.mongoConnectionString, logger); var exchangeapi = new exchangeapiservice(config.exchangeSettings, config.apiSettings, logger); -var retriever = new dataretriever(config.downloaderRefreshSeconds, exchangeapi, logger); +var retriever = new dataretriever(config.downloaderRefreshSeconds, config.downloaderMaxFails, exchangeapi, logger); var processor = new dataprocessor(storage, logger); var aggregator = new candleaggregator(config.indicatorSettings.candleStickSizeMinutes, storage, logger); var advisor = new tradingadvisor(config.indicatorSettings, storage, logger); diff --git a/config.sample.js b/config.sample.js index 420be9b..e8faa8a 100644 --- a/config.sample.js +++ b/config.sample.js @@ -45,6 +45,8 @@ config.mongoConnectionString = 'localhost/bitbot'; //------------------------------downloaderSettings config.downloaderRefreshSeconds = 10; // Best to keep this default setting unless you know what you are doing. +config.downloaderMaxFails = 30; +// After failing to retrieve trade data n times the application will quit. A value of 0 prevents quitting. //------------------------------downloaderSettings //------------------------------IndicatorSettings diff --git a/services/dataretriever.js b/services/dataretriever.js index fd201ed..c219c81 100644 --- a/services/dataretriever.js +++ b/services/dataretriever.js @@ -1,9 +1,10 @@ var _ = require('underscore'); var async = require('async'); -var downloader = function(refreshInterval, exchangeapi, logger){ +var downloader = function(refreshInterval, maxFails, exchangeapi, logger){ this.refreshInterval = refreshInterval; + this.maxFails = maxFails; this.noTradesCount = 0; this.exchangeapi = exchangeapi; this.logger = logger; @@ -30,8 +31,8 @@ downloader.prototype.processTrades = function(err, trades) { this.noTradesCount += 1; - if(this.noTradesCount >= 30) { - this.logger.error('Haven\'t received data from the Exchange API for 30 consecutive attempts, stopping application'); + if(this.maxFails && this.noTradesCount >= this.maxFails) { + this.logger.error('Haven\'t received data from the Exchange API for '+ this.noTradesCount +' consecutive attempts, quitting application'); return process.exit(); } From 9d36b192df9de2121629fc63e80a6a6615e8f497 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert <5an1ty@users.noreply.github.com> Date: Sun, 6 Aug 2017 10:49:55 +0200 Subject: [PATCH 55/57] update kraken-exchange-api dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd2bd33..2d70963 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "mongojs": "0.18.0", "pushover-notifications": "0.2.2", "underscore": "1.7.0", - "kraken-exchange-api": "0.1.0", + "kraken-exchange-api": "1.0.0", "btc-e": "1.0.5", "winston": "0.8.3" }, From 7802eebaad9bd1f5b42f311f8e527bbd551dbcbf Mon Sep 17 00:00:00 2001 From: Ruben Callewaert <5an1ty@users.noreply.github.com> Date: Tue, 22 Aug 2017 13:17:14 +0200 Subject: [PATCH 56/57] Dependencies refresher --- package-lock.json | 774 +++++++++++++++++++++++++++++++++++++ package.json | 21 +- services/loggingservice.js | 2 +- 3 files changed, 786 insertions(+), 11 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1ae5eae --- /dev/null +++ b/package-lock.json @@ -0,0 +1,774 @@ +{ + "name": "BitBot", + "version": "0.9.7", + "lockfileVersion": 1, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true + }, + "bitstamp-api": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bitstamp-api/-/bitstamp-api-0.2.0.tgz", + "integrity": "sha1-6JBRJrA4F8BTGhjPi+ejz//canY=" + }, + "bl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" + }, + "bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", + "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" + }, + "btc-e": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/btc-e/-/btc-e-1.0.5.tgz", + "integrity": "sha1-bEregFNaXvoAg1rkBfyZIbt7JgY=", + "dependencies": { + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=" + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=" + }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, + "aws-sign2": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=" + }, + "bl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=" + }, + "boom": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", + "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=" + }, + "caseless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.8.0.tgz", + "integrity": "sha1-W8oogdQUN/VLJAfr40iIx7mtT30=" + }, + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=" + }, + "cryptiles": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", + "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=" + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" + }, + "forever-agent": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", + "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=" + }, + "form-data": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", + "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=" + }, + "hawk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", + "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=" + }, + "hoek": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", + "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=" + }, + "http-signature": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, + "mime-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", + "integrity": "sha1-mVrhOSq4r/y/yyZB3QVOlDwNXc4=" + }, + "oauth-sign": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.5.0.tgz", + "integrity": "sha1-12f1FpMlYg6rLgh+8MRy53PbZGE=" + }, + "qs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", + "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=" + }, + "request": { + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.49.0.tgz", + "integrity": "sha1-DU9jSNwzSAWbVT5Ntg/SR43mYqc=" + }, + "sntp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", + "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=" + } + } + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" + }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=" + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/each-series/-/each-series-1.0.0.tgz", + "integrity": "sha1-+Ibmxm39sl7x/nNWQUbuXLR4r8s=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true + }, + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "flat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz", + "integrity": "sha512-ji/WMv2jdsE+LaznpkIF9Haax0sdpTBozrz/Dtg4qSRMfbs8oVg4ypJunIRYPiMLvH/ed6OflXbnbTIKJhtgeg==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=" + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-my-json-valid": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "kraken-exchange-api": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kraken-exchange-api/-/kraken-exchange-api-1.0.0.tgz", + "integrity": "sha512-HlrFFVIuz9LTAj+ZCCCE6MFCtgsHLkAbhasJmu/jqTvJe42HSmUfEIspjGekYwpiLHO246RXsdrkP6TOD5YU7Q==", + "dependencies": { + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=" + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + } + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "mime": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", + "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=" + }, + "mime-db": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", + "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" + }, + "mime-types": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", + "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=" + }, + "moment": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", + "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + }, + "mongodb": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", + "integrity": "sha1-GUBEXGYeGSF7s7+CRdmFSq71SNs=", + "dependencies": { + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=" + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==" + } + } + }, + "mongodb-core": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", + "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=" + }, + "mongojs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-2.4.1.tgz", + "integrity": "sha512-R344Q8ufjcqyFHO1CrxYboUBrEJwmsvMBtI8wsjCZq90mh/lzT0PBleAD6d1f8s07zeHSM2ebeu3OwMC4wxQlg==" + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + }, + "parse-mongo-url": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-mongo-url/-/parse-mongo-url-1.1.1.tgz", + "integrity": "sha1-ZiON9fjnwMjKTNlw1KtqE3PrdbU=" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "pushover-notifications": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-0.2.4.tgz", + "integrity": "sha1-icuU+RgsNqpZqaxnROSa2K/00Gc=" + }, + "qs": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.0.4.tgz", + "integrity": "sha1-UQGdhHIMk5uCc36EVWp4Izjs6ns=" + }, + "query-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.0.0.tgz", + "integrity": "sha1-+99wBLTSr/eS+YcZgbeieU9VWUc=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=" + }, + "request": { + "version": "2.69.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.69.0.tgz", + "integrity": "sha1-z5HS4AB1KxIXFVwAUkGRGZGiNGo=" + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==" + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "thunky": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", + "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=" + }, + "to-mongodb-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-mongodb-core/-/to-mongodb-core-2.0.0.tgz", + "integrity": "sha1-NZbsdhOsmtO5ioncua77pWnNJ+s=" + }, + "tough-cookie": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", + "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=" + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "winston": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.3.1.tgz", + "integrity": "sha1-C0hCDZeMAYBM8CMLZIhhWYIloRk=", + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + } + } + }, + "winston-daily-rotate-file": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-1.4.6.tgz", + "integrity": "sha1-8gS2raGaU4b99S/pl9jhDkP/d4g=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/package.json b/package.json index 2d70963..9d79ca7 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,18 @@ "description": "Crypto-Currency Trading Bot", "main": "app.js", "dependencies": { - "async": "0.9.0", - "flat": "1.5.0", - "bitstamp-api": "0.1.9", - "mime": "1.2.11", - "moment": "2.9.0", - "mongojs": "0.18.0", - "pushover-notifications": "0.2.2", - "underscore": "1.7.0", - "kraken-exchange-api": "1.0.0", + "async": "2.5.0", + "bitstamp-api": "0.2.0", "btc-e": "1.0.5", - "winston": "0.8.3" + "flat": "4.0.0", + "kraken-exchange-api": "1.0.0", + "mime": "1.3.6", + "moment": "2.18.1", + "mongojs": "2.4.1", + "pushover-notifications": "0.2.4", + "underscore": "1.8.3", + "winston": "2.3.1", + "winston-daily-rotate-file": "^1.4.6" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/services/loggingservice.js b/services/loggingservice.js index dac0137..9c4c1be 100644 --- a/services/loggingservice.js +++ b/services/loggingservice.js @@ -36,7 +36,7 @@ var logger = function(app, debug, prefix) { new (winston.transports.Console)({ 'timestamp': now, level: 'INFO' }), - new (winston.transports.DailyRotateFile)({ + new (require('winston-daily-rotate-file'))({ 'timestamp': now, datePattern: '_dd-MM-yyyy.log', filename: 'logs/' + app, From 3dd11f865566a6a762a0f2be2cf99bff87f8f144 Mon Sep 17 00:00:00 2001 From: Ruben Callewaert <5an1ty@users.noreply.github.com> Date: Tue, 22 Aug 2017 13:29:36 +0200 Subject: [PATCH 57/57] push data in chunks of 1000 --- services/storage.js | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/services/storage.js b/services/storage.js index dd1ea81..a0b214e 100644 --- a/services/storage.js +++ b/services/storage.js @@ -20,26 +20,33 @@ storage.prototype.push = function(csArray, callback) { var csDatastore = mongo(this.mongoConnectionString); var csCollection = csDatastore.collection(this.exchangePair); - var bulk = csCollection.initializeOrderedBulkOp(); + function chunk (arr, len) { + var chunks = [], + i = 0, + n = arr.length; - _.forEach(csArray, function(cs) { - bulk.find({period: cs.period}).upsert().updateOne(cs); - }); - - bulk.execute(function(err, res) { - - csDatastore.close(); - - if(err) { + while (i < n) { + chunks.push(arr.slice(i, i += len)); + } - callback(err); + return chunks; + } - } else { + var chunks = chunk(csArray, 1000); - callback(null); + async.eachSeries(chunks, function(chunk, next) { + var bulk = csCollection.initializeOrderedBulkOp(); - } + _.forEach(chunk, function(cs) { + bulk.find({period: cs.period}).upsert().updateOne(cs); + }); + bulk.execute(function(err, res) { + next(err); + }); + }, function(err) { + csDatastore.close(); + callback(err); }); };