diff --git a/.gitignore b/.gitignore index 410e87b53..6bff731ea 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # OS or Editor folders .DS_Store Thumbs.db +config.js .cache .project .settings @@ -39,3 +40,4 @@ node_modules candles.csv cexio.db history +TMP_* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 87f8cd91a..1cd6caba7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ language: node_js node_js: - - "0.10" \ No newline at end of file + - "4.2.1" +before_install: + - cp sample-config.js config.js \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..63ad3f204 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +Thanks for (thinking about) contributing to Gekko, all help is wanted! + +- If you want to add an exchange to Gekko, see [this doc](https://github.com/askmike/gekko/blob/develop/docs/internals/exchanges.md) for all the information you need. +- If you want to Gekko react to anything from the market, you can most likely put this functionality into a plugin. See [this document](https://github.com/askmike/gekko/blob/develop/docs/internals/plugins.md) for details. +- If you want to add trading strategies / indicators, please see [this document](https://github.com/askmike/gekko/blob/develop/docs/internals/trading_methods.md). +- If you just want to work on Gekko, you can use the open issues with the tag `open-for-pulls` for inspiration. + +Things to take into consideration when submitting a pull request: + + - Please submit all pull requests (except for hotfixes) to the [develop branch](https://github.com/askmike/gekko/tree/develop). + - Please keep current code styling in mind. \ No newline at end of file diff --git a/core/budfox/budfox.js b/core/budfox/budfox.js new file mode 100644 index 000000000..8ae305518 --- /dev/null +++ b/core/budfox/budfox.js @@ -0,0 +1,93 @@ +// Budfox is the realtime market for Gekko! +// +// Read more here: +// @link https://github.com/askmike/gekko/blob/stable/docs/internals/budfox.md +// +// > [getting up] I don't know. I guess I realized that I'm just Bud Fox. +// > As much as I wanted to be Gordon Gekko, I'll *always* be Bud Fox. +// > [tosses back the handkerchief and walks away] + +var _ = require('lodash'); +var async = require('async'); + +var util = require(__dirname + '/../util'); +var dirs = util.dirs(); + +var Heart = require(dirs.budfox + 'heart'); +var MarketDataProvider = require(dirs.budfox + 'marketDataProvider'); +var CandleManager = require(dirs.budfox + 'candleManager'); + +var BudFox = function(config) { + _.bindAll(this); + + Readable.call(this, {objectMode: true}); + + // BudFox internal modules: + + this.heart = new Heart; + this.marketDataProvider = new MarketDataProvider(config); + this.candleManager = new CandleManager; + + // BudFox data flow: + + // on every `tick` retrieve trade data + this.heart.on( + 'tick', + this.marketDataProvider.retrieve + ); + + // on new trade data create candles + this.marketDataProvider.on( + 'trades', + this.candleManager.processTrades + ); + + // Output the candles + this.candleManager.on( + 'candles', + this.pushCandles + ); + + this.heart.pump(); + + // Budfox also reports: + + // Trades & last trade + // + // this.marketDataProvider.on( + // 'trades', + // this.broadcast('trades') + // ); + // this.marketDataProvider.on( + // 'trades', + // this.broadcastTrade + // ); +} + +var Readable = require('stream').Readable; + +BudFox.prototype = Object.create(Readable.prototype, { + constructor: { value: BudFox } +}); + +BudFox.prototype._read = function noop() {} + +BudFox.prototype.pushCandles = function(candles) { + _.each(candles, this.push); +} + +// BudFox.prototype.broadcastTrade = function(trades) { +// _.defer(function() { +// this.emit('trade', trades.last); +// }.bind(this)); +// } + +// BudFox.prototype.broadcast = function(message) { +// return function(payload) { +// _.defer(function() { +// this.emit(message, payload); +// }.bind(this)); +// }.bind(this); +// } + +module.exports = BudFox; diff --git a/core/budfox/candleCreator.js b/core/budfox/candleCreator.js new file mode 100644 index 000000000..e2ae35dcc --- /dev/null +++ b/core/budfox/candleCreator.js @@ -0,0 +1,197 @@ +// The CandleCreator creates one minute candles based on trade batches. Note +// that it also adds empty candles to fill gaps with no volume. +// +// Expects trade batches to be written like: +// +// { +// amount: x, +// start: (moment), +// end: (moment), +// first: (trade), +// last: (trade), +// timespan: x, +// all: [ +// // batch of new trades with +// // moments instead of timestamps +// ] +// } +// +// Emits 'new candles' event with: +// +// [ +// { +// start: (moment), +// end: (moment), +// high: (float), +// open: (float), +// low: (float), +// close: (float) +// volume: (float) +// vwp: (float) // volume weighted price +// }, +// { +// start: (moment), // + 1 +// end: (moment), +// high: (float), +// open: (float), +// low: (float), +// close: (float) +// volume: (float) +// vwp: (float) // volume weighted price +// } +// // etc. +// ] +// + +var _ = require('lodash'); +var moment = require('moment'); + +var util = require(__dirname + '/../util'); + +var CandleCreator = function() { + _.bindAll(this); + + // TODO: remove fixed date + this.threshold = moment("1970-01-01", "YYYY-MM-DD"); + + // This also holds the leftover between fetches + this.buckets = {}; +} + +util.makeEventEmitter(CandleCreator); + +CandleCreator.prototype.write = function(batch) { + var trades = batch.data; + + if(_.isEmpty(trades)) + return; + + trades = this.filter(trades); + this.fillBuckets(trades); + var candles = this.calculateCandles(); + + candles = this.addEmptyCandles(candles); + + // the last candle is not complete + this.threshold = candles.pop().start; + + this.emit('candles', candles); +} + +CandleCreator.prototype.filter = function(trades) { + // make sure we only include trades more recent + // than the previous emitted candle + return _.filter(trades, function(trade) { + return trade.date > this.threshold; + }, this); +} + +// put each trade in a per minute bucket +CandleCreator.prototype.fillBuckets = function(trades) { + _.each(trades, function(trade) { + var minute = trade.date.format('YYYY-MM-DD HH:mm'); + + if(!(minute in this.buckets)) + this.buckets[minute] = []; + + this.buckets[minute].push(trade); + }, this); + + this.lastTrade = _.last(trades); +} + +// convert each bucket into a candle +CandleCreator.prototype.calculateCandles = function() { + var minutes = _.size(this.buckets); + + // catch error from high volume getTrades + if (this.lastTrade !== undefined) + // create a string referencing to minute this trade happened in + var lastMinute = this.lastTrade.date.format('YYYY-MM-DD HH:mm'); + + var candles = _.map(this.buckets, function(bucket, name) { + var candle = this.calculateCandle(bucket); + + // clean all buckets, except the last one: + // this candle is not complete + if(name !== lastMinute) + delete this.buckets[name]; + + return candle; + }, this); + + return candles; +} + +CandleCreator.prototype.calculateCandle = function(trades) { + var first = _.first(trades); + + var f = parseFloat; + + var candle = { + start: first.date.clone().startOf('minute'), + open: f(first.price), + high: f(first.price), + low: f(first.price), + close: f(_.last(trades).price), + vwp: 0, + volume: 0, + trades: _.size(trades) + }; + + _.each(trades, function(trade) { + candle.high = _.max([candle.high, f(trade.price)]); + candle.low = _.min([candle.low, f(trade.price)]); + candle.volume += f(trade.amount); + candle.vwp += f(trade.price) * f(trade.amount); + }); + + candle.vwp /= candle.volume; + + return candle; +} + +// Gekko expects a candle every minute, if nothing happened +// during a particilar minute Gekko will add empty candles with: +// +// - open, high, close, low, vwp are the same as the close of the previous candle. +// - trades, volume are 0 +CandleCreator.prototype.addEmptyCandles = function(candles) { + var amount = _.size(candles); + if(!amount) + return candles; + + // iterator + var start = _.first(candles).start.clone(); + var end = _.last(candles).start; + var i, j = -1; + + var minutes = _.map(candles, function(candle) { + return +candle.start; + }); + + while(start < end) { + start.add('minute', 1); + i = +start; + j++; + + if(_.contains(minutes, i)) + continue; // we have a candle for this minute + + var lastPrice = candles[j].close; + + candles.splice(j + 1, 0, { + start: start.clone(), + open: lastPrice, + high: lastPrice, + low: lastPrice, + close: lastPrice, + vwp: lastPrice, + volume: 0, + trades: 0 + }); + } + return candles; +} + +module.exports = CandleCreator; diff --git a/core/budfox/candleManager.js b/core/budfox/candleManager.js new file mode 100644 index 000000000..b90cc692d --- /dev/null +++ b/core/budfox/candleManager.js @@ -0,0 +1,34 @@ +// The candleManager consumes trades and emits: +// - `candles`: array of minutly candles. +// - `candle`: the most recent candle after a fetch Gekko. + +var _ = require('lodash'); +var moment = require('moment'); +var fs = require('fs'); + +var util = require(__dirname + '/../util'); +var dirs = util.dirs(); +var config = util.getConfig(); +var log = require(dirs.core + 'log'); + +var CandleCreator = require(dirs.budfox + 'candleCreator'); + +var Manager = function() { + _.bindAll(this); + + this.candleCreator = new CandleCreator; + + this.candleCreator + .on('candles', this.relayCandles); +}; + +util.makeEventEmitter(Manager); +Manager.prototype.processTrades = function(tradeBatch) { + this.candleCreator.write(tradeBatch); +} + +Manager.prototype.relayCandles = function(candles) { + this.emit('candles', candles); +} + +module.exports = Manager; diff --git a/core/budfox/heart.js b/core/budfox/heart.js new file mode 100644 index 000000000..2b5ad943b --- /dev/null +++ b/core/budfox/heart.js @@ -0,0 +1,42 @@ +// The heart schedules and emit ticks every 20 seconds. + +var util = require(__dirname + '/../util'); +var log = require(util.dirs().core + 'log'); + +var _ = require('lodash'); +var moment = require('moment'); + +var Heart = function() { + _.bindAll(this); +} + +util.makeEventEmitter(Heart); + +Heart.prototype.pump = function() { + log.debug('scheduling ticks'); + this.scheduleTicks(); +} + +Heart.prototype.tick = function() { + this.emit('tick'); +} + +Heart.prototype.determineLiveTickRate = function() { + // TODO: fix + if(util.getConfig().watch.exchange === 'okcoin') + var seconds = 2; + else + var seconds = 20; + + this.tickRate = +moment.duration(seconds, 's'); +} + +Heart.prototype.scheduleTicks = function() { + this.determineLiveTickRate(); + setInterval(this.tick, this.tickRate); + + // start! + _.defer(this.tick); +} + +module.exports = Heart; diff --git a/core/budfox/marketDataProvider.js b/core/budfox/marketDataProvider.js new file mode 100644 index 000000000..1e66bfb25 --- /dev/null +++ b/core/budfox/marketDataProvider.js @@ -0,0 +1,37 @@ +// +// The market data provider will fetch data from a datasource on tick. It emits: +// +// - `trades`: batch of newly detected trades +// - `trade`: after Gekko fetched new trades, this +// will be the most recent one. + +var _ = require('lodash'); +var util = require(__dirname + '/../util'); + +var MarketFetcher = require('./marketFetcher'); + +var Manager = function(config) { + + _.bindAll(this); + + // fetch trades + this.source = new MarketFetcher(config); + + // relay newly fetched trades + this.source + .on('trades batch', this.relayTrades); +} + +util.makeEventEmitter(Manager); + +// HANDLERS +Manager.prototype.retrieve = function() { + this.source.fetch(); +} + + +Manager.prototype.relayTrades = function(batch) { + this.emit('trades', batch); +} + +module.exports = Manager; \ No newline at end of file diff --git a/core/budfox/marketFetcher.js b/core/budfox/marketFetcher.js new file mode 100644 index 000000000..f8e3c6d93 --- /dev/null +++ b/core/budfox/marketFetcher.js @@ -0,0 +1,103 @@ +// +// The fetcher is responsible for fetching new +// market data at the exchange on interval. It will emit +// the following events: +// +// - `trades batch` - all new trades. +// - `trade` - the most recent trade after every fetch + +var _ = require('lodash'); +var moment = require('moment'); +var utc = moment.utc; +var util = require(__dirname + '/../util'); + +var config = util.getConfig(); +var log = require(util.dirs().core + 'log'); +var exchangeChecker = require(util.dirs().core + 'exchangeChecker'); + +var TradeBatcher = require(util.dirs().budfox + 'tradeBatcher'); + +var Fetcher = function(config) { + + var provider = config.watch.exchange.toLowerCase(); + var DataProvider = require(util.dirs().gekko + 'exchanges/' + provider); + _.bindAll(this); + + // Create a public dataProvider object which can retrieve live + // trade information from an exchange. + this.watcher = new DataProvider(config.watch); + + this.exchange = exchangeChecker.settings(config.watch); + + var requiredHistory = config.tradingAdvisor.candleSize * config.tradingAdvisor.historySize; + + // If the trading adviser is enabled we might need a very specific fetch since + // to line up [local db, trading method, and fetching] + if(config.tradingAdvisor.enabled && config.tradingAdvisor.firstFetchSince) { + this.firstSince = config.tradingAdvisor.firstFetchSince; + } + + this.batcher = new TradeBatcher(this.exchange.tid); + + this.pair = [ + config.asset, + config.currency + ].join('/'); + + log.info('Starting to watch the market:', + this.exchange.name, + this.pair + ); + + // if the exchange returns an error + // we will keep on retrying until next + // scheduled fetch. + this.tries = 0; + this.limit = 20; // [TODO] + + this.firstFetch = true; + + this.batcher.on('new batch', this.relayTrades); +} + +util.makeEventEmitter(Fetcher); + +Fetcher.prototype._fetch = function(since) { + if(++this.tries >= this.limit) + return; + + this.watcher.getTrades(since, this.processTrades, false); +} + +Fetcher.prototype.fetch = function() { + var since = false; + if(this.firstFetch) { + since = this.firstSince; + this.firstFetch = false; + } else + since = false; + + this.tries = 0; + log.debug('Requested', this.pair, 'trade data from', this.exchange.name, '...'); + this._fetch(since); +} + +Fetcher.prototype.processTrades = function(err, trades) { + if(err || _.isEmpty(trades)) { + if(err) { + log.warn(this.exhange.name, 'returned an error while fetching trades:', err); + log.debug('refetching...'); + } else + log.debug('Trade fetch came back empty, refetching...'); + setTimeout(this._fetch, +moment.duration('s', 1)); + return; + } + + this.batcher.write(trades); +} + +Fetcher.prototype.relayTrades = function(batch) { + this.emit('trades batch', batch); +} + +module.exports = Fetcher; diff --git a/core/budfox/tradeBatcher.js b/core/budfox/tradeBatcher.js new file mode 100644 index 000000000..dca0d9b15 --- /dev/null +++ b/core/budfox/tradeBatcher.js @@ -0,0 +1,117 @@ +// +// Small wrapper that only propogates new trades. +// +// Expects trade batches to be written like: +// [ +// { +// tid: x, // tid is preferred, but if none available date will also work +// price: x, +// date: (timestamp), +// amount: x +// }, +// { +// tid: x + 1, +// price: x, +// date: (timestamp), +// amount: x +// } +// ] +// +// Emits 'new trades' event with: +// { +// amount: x, +// start: (moment), +// end: (moment), +// first: (trade), +// last: (trade) +// data: [ +// // batch of new trades with +// // moments instead of timestamps +// ] +// } + +var _ = require('lodash'); +var moment = require('moment'); +var util = require('../util'); +var log = require('../log'); + +var TradeBatcher = function(tid) { + if(!_.isString(tid)) + throw 'tid is not a string'; + + _.bindAll(this); + this.tid = tid; + this.last = -1; +} + +util.makeEventEmitter(TradeBatcher); + +TradeBatcher.prototype.write = function(batch) { + + if(!_.isArray(batch)) + throw 'batch is not an array'; + + if(_.isEmpty(batch)) + return log.debug('Trade fetch came back empty.'); + + var filterBatch = this.filter(batch); + + var amount = _.size(filterBatch); + if(!amount) + return log.debug('No new trades.'); + + var momentBatch = this.convertDates(filterBatch); + + var last = _.last(momentBatch); + var first = _.first(momentBatch); + + log.debug( + 'Processing', amount, 'new trades.', + 'From', + first.date.format('YYYY-MM-DD HH:mm:ss'), + 'UTC to', + last.date.format('YYYY-MM-DD HH:mm:ss'), + 'UTC.', + '(' + first.date.from(last.date, true) + ')' + ); + + this.emit('new batch', { + amount: amount, + start: first.date, + end: last.date, + last: last, + first: first, + data: momentBatch + }); + + this.last = last[this.tid]; + + // we overwrote those, get unix ts back + if(this.tid === 'date') + this.last = this.last.unix(); + +} + +TradeBatcher.prototype.filter = function(batch) { + // make sure we're not trying to count + // beyond infinity + var lastTid = _.last(batch)[this.tid]; + if(lastTid === lastTid + 1) + util.die('trade tid is max int, Gekko can\'t process..'); + + // weed out known trades + // TODO: optimize by stopping as soon as the + // first trade is too old (reverse first) + return _.filter(batch, function(trade) { + return this.last < trade[this.tid]; + }, this); +} + +TradeBatcher.prototype.convertDates = function(batch) { + return _.map(_.cloneDeep(batch), function(trade) { + trade.date = moment.unix(trade.date).utc(); + return trade; + }); +} + +module.exports = TradeBatcher; diff --git a/core/candleBatcher.js b/core/candleBatcher.js new file mode 100644 index 000000000..53b834aef --- /dev/null +++ b/core/candleBatcher.js @@ -0,0 +1,75 @@ +// internally we only use 1m +// candles, this can easily +// convert them to any desired +// size. + +// Acts as ~fake~ stream: takes +// 1m candles as input and emits +// bigger candles. +// +// input are transported candles. + +var _ = require('lodash'); +var util = require(__dirname + '/util'); + +var CandleBatcher = function(candleSize) { + if(!_.isNumber(candleSize)) + throw 'candleSize is not a number'; + + this.candleSize = candleSize; + this.smallCandles = []; +} + +util.makeEventEmitter(CandleBatcher); + +CandleBatcher.prototype.write = function(candles) { + if(!_.isArray(candles)) + throw 'candles is not an array'; + + _.each(candles, function(candle) { + this.smallCandles.push(candle); + this.check(); + }, this); +} + +CandleBatcher.prototype.check = function() { + if(_.size(this.smallCandles) % this.candleSize !== 0) + return; + + this.emit('candle', this.calculate()); + this.smallCandles = []; +} + +CandleBatcher.prototype.calculate = function() { + var first = this.smallCandles.shift(); + + first.vwp = first.vwp * first.volume; + + var candle = _.reduce( + this.smallCandles, + function(candle, m) { + candle.high = _.max([candle.high, m.high]); + candle.low = _.min([candle.low, m.low]); + candle.close = m.close; + candle.volume += m.volume; + candle.vwp += m.vwp * m.volume; + candle.trades += m.trades; + return candle; + }, + first + ); + + if(candle.volume) + // we have added up all prices (relative to volume) + // now divide by volume to get the Volume Weighted Price + candle.vwp /= candle.volume; + else + // empty candle + candle.vwp = candle.open; + + candle.start = first.start; + + return candle; +} + +module.exports = CandleBatcher; diff --git a/core/eventLogger.js b/core/eventLogger.js new file mode 100644 index 000000000..addc5fc2c --- /dev/null +++ b/core/eventLogger.js @@ -0,0 +1,32 @@ +var _ = require('lodash'); + +var util = require('./util'); +var dirs = util.dirs(); +var log = require(dirs.core + 'log'); + +var EventLogger = function() { + _.bindAll(this); +} + +var subscriptions = require(dirs.core + 'subscriptions'); +_.each(subscriptions, function(subscription) { + EventLogger.prototype[subscription.handler] = function(e) { + if(subscription.event === 'tick') + log.empty(); + + if(_.has(e, 'data')) + log.debug( + '\tnew event:', + subscription.event, + '(' + _.size(e.data), + 'items)' + ); + else + log.debug( + '\tnew event:', + subscription.event + ); + } +}); + +module.exports = EventLogger; diff --git a/core/gekko-child.js b/core/gekko-child.js new file mode 100644 index 000000000..d5bfc6958 --- /dev/null +++ b/core/gekko-child.js @@ -0,0 +1,50 @@ +/* + + Gekko is a Bitcoin trading bot for popular Bitcoin exchanges written + in node, it features multiple trading methods using technical analysis. + + If you are interested in how Gekko works, read more about Gekko's + architecture here: + + https://github.com/askmike/gekko/blob/stable/docs/internals/architecture.md + + Disclaimer: + + USE AT YOUR OWN RISK! + + The author of this project is NOT responsible for any damage or loss caused + by this software. There can be bugs and the bot may not perform as expected + or specified. Please consider testing it first with paper trading and/or + backtesting on historical data. Also look at the code to see what how + it is working. + +*/ + +var util = require(__dirname + '/util'); + +var dirs = util.dirs(); +var ipc = require('relieve').IPCEE(process); + +var config; +var mode; + +ipc.on('start', (mode, config) => { + + // force correct gekko env + util.setGekkoEnv('child-process'); + + // force correct gekko mode + util.setGekkoMode(mode); + + // force disable debug + config.debug = false; + util.setConfig(config); + + var pipeline = require(dirs.core + 'pipeline'); + pipeline({ + config: config, + mode: mode + }); +}); + + diff --git a/core/gekko-controller.js b/core/gekko-controller.js new file mode 100644 index 000000000..22de74ebe --- /dev/null +++ b/core/gekko-controller.js @@ -0,0 +1,19 @@ +// WIP: +// usage +// +// in gekko dir: +// node core/gekko-controller + +var ForkTask = require('relieve').tasks.ForkTask +var fork = require('child_process').fork + +task = new ForkTask(fork('./core/gekko-child.js')); + +var mode = 'backtest'; +var config = require('../config'); + +task.send('start', mode, config); + +task.on('log', function(data) { + console.log('CHILD LOG:', data); +}); \ No newline at end of file diff --git a/core/gekkoStream.js b/core/gekkoStream.js new file mode 100644 index 000000000..1ee77609d --- /dev/null +++ b/core/gekkoStream.js @@ -0,0 +1,51 @@ +// Small writable stream wrapper that +// passes data to all `candleConsumers`. + +var Writable = require('stream').Writable; +var _ = require('lodash'); + +var util = require('./util'); +var env = util.gekkoEnv(); +var mode = util.gekkoMode(); + +var Gekko = function(candleConsumers) { + this.candleConsumers = candleConsumers; + Writable.call(this, {objectMode: true}); + + this.finalize = _.bind(this.finalize, this); +} + +Gekko.prototype = Object.create(Writable.prototype, { + constructor: { value: Gekko } +}); + +Gekko.prototype._write = function(chunk, encoding, _done) { + var done = _.after(this.candleConsumers.length, _done); + _.each(this.candleConsumers, function(c) { + c.processCandle(chunk, done); + }); +} + +Gekko.prototype.finalize = function() { + var tradingMethod = _.find( + this.candleConsumers, + c => c.meta.name === 'Trading Advisor' + ); + + if(!tradingMethod) + return this.shutdown(); + + tradingMethod.finish(this.shutdown.bind(this)); +} + +Gekko.prototype.shutdown = function() { + _.each(this.candleConsumers, function(c) { + if(c.finalize) + c.finalize(); + }); + + if(env === 'child-process') + process.exit(0); +} + +module.exports = Gekko; \ No newline at end of file diff --git a/core/log.js b/core/log.js index 0fd03f2aa..2068a8e98 100644 --- a/core/log.js +++ b/core/log.js @@ -9,10 +9,34 @@ var moment = require('moment'); var fmt = require('util').format; var _ = require('lodash'); -var debug = require('./util').getConfig().debug; +var util = require('./util'); +var debug = util.getConfig().debug; + +var sendIPC = function() { + var IPCEE = require('relieve').IPCEE + var ipc = IPCEE(process); + + var send = function(method) { + return function() { + var args = _.toArray(arguments); + ipc.send('log', args.join(' ')); + } + } + + return { + error: send('error'), + warn: send('warn'), + info: send('info') + } +} var Log = function() { _.bindAll(this); + this.env = util.gekkoEnv(); + if(this.env === 'standalone') + this.output = console; + else if(this.env === 'child-process') + this.output = sendIPC(); }; Log.prototype = { @@ -24,7 +48,7 @@ Log.prototype = { message += ' (' + name + '):\t'; message += fmt.apply(null, args); - console[method](message); + this.output[method](message); }, error: function() { this._write('error', arguments); @@ -42,6 +66,6 @@ if(debug) this._write('info', arguments, 'DEBUG'); } else - Log.prototype.debug = function() {}; + Log.prototype.debug = _.noop; module.exports = new Log; \ No newline at end of file diff --git a/core/markets/backtest.js b/core/markets/backtest.js new file mode 100644 index 000000000..41d399359 --- /dev/null +++ b/core/markets/backtest.js @@ -0,0 +1,105 @@ +var _ = require('lodash'); +var util = require('../util'); +var config = util.getConfig(); +var dirs = util.dirs(); +var log = require(dirs.core + 'log'); +var moment = require('moment'); + +var adapter = config.adapters[config.backtest.adapter]; +var Reader = require(dirs.gekko + adapter.path + '/reader'); +var daterange = config.backtest.daterange; + +var to = moment.utc(daterange.to); +var from = moment.utc(daterange.from); + +if(to <= from) + util.die('This daterange does not make sense.') + +var Market = function() { + _.bindAll(this); + this.pushing = false; + this.ended = false; + + Readable.call(this, {objectMode: true}); + + console.log(''); + log.info('\tWARNING: BACKTESTING FEATURE NEEDS PROPER TESTING'); + log.info('\tWARNING: ACT ON THESE NUMBERS AT YOUR OWN RISK!'); + console.log(''); + + this.reader = new Reader(); + this.batchSize = config.backtest.batchSize; + this.iterator = { + from: from.clone(), + to: from.clone().add(this.batchSize, 'm').subtract(1, 's') + } +} + +var Readable = require('stream').Readable; +Market.prototype = Object.create(Readable.prototype, { + constructor: { value: Market } +}); + +Market.prototype._read = function() { + if(this.pushing) + return; + + this.get(); +} + +Market.prototype.get = function() { + if(this.iterator.to >= to) { + this.iterator.to = to; + this.ended = true; + } + + this.reader.get( + this.iterator.from.unix(), + this.iterator.to.unix(), + 'full', + this.processCandles + ) +} + +Market.prototype.processCandles = function(err, candles) { + this.pushing = true; + var amount = _.size(candles); + + if(amount === 0) + util.die('Query returned no candles (do you have local data for the specified range?)'); + + if(!this.ended && amount < this.batchSize) { + var d = function(ts) { + return moment.unix(ts).utc().format('YYYY-MM-DD HH:mm:ss'); + } + var from = d(_.first(candles).start); + var to = d(_.last(candles).start); + log.warn(`Simulation based on incomplete market data (${this.batchSize - amount} missing between ${from} and ${to}).`); + } + + _.each(candles, function(c, i) { + c.start = moment.unix(c.start); + + if(++i === amount) { + // last one candle from batch + if(!this.ended) + this.pushing = false; + else { + _.defer(function() { + this.reader.close(); + this.emit('end'); + }.bind(this)); + } + } + + this.push(c); + + }, this); + + this.iterator = { + from: this.iterator.from.clone().add(this.batchSize, 'm'), + to: this.iterator.from.clone().add(this.batchSize * 2, 'm').subtract(1, 's') + } +} + +module.exports = Market; \ No newline at end of file diff --git a/core/markets/importer.js b/core/markets/importer.js new file mode 100644 index 000000000..0d365e425 --- /dev/null +++ b/core/markets/importer.js @@ -0,0 +1,100 @@ +var _ = require('lodash'); +var util = require('../util'); +var config = util.getConfig(); +var dirs = util.dirs(); +var log = require(dirs.core + 'log'); +var moment = require('moment'); + +var adapter = config.adapters[config.importer.adapter]; +var daterange = config.importer.daterange; + +var from = moment.utc(daterange.from); + +if(daterange.to) { + var to = moment.utc(daterange.to); +} else{ + var to = moment().utc(); + log.debug( + 'No end date specified for importing, setting to', + to.format('YYYY-MM-DD HH:mm:ss') + ); +} + +var TradeBatcher = require(dirs.budfox + 'tradeBatcher'); +var CandleManager = require(dirs.budfox + 'candleManager'); +var exchangeChecker = require(dirs.core + 'exchangeChecker'); + +var error = exchangeChecker.cantFetchFullHistory(config.watch); +if(error) + util.die(error, true); + +var fetcher = require(dirs.importers + config.watch.exchange); + +if(to <= from) + util.die('This daterange does not make sense.') + +var Market = function() { + _.bindAll(this); + this.exchangeSettings = exchangeChecker.settings(config.watch); + + this.tradeBatcher = new TradeBatcher(this.exchangeSettings.tid); + this.candleManager = new CandleManager; + this.fetcher = fetcher({ + to: to, + from: from + }); + + this.done = false; + + this.fetcher.bus.on( + 'trades', + this.processTrades + ); + + this.fetcher.bus.on( + 'done', + function() { + this.done = true; + }.bind(this) + ) + + this.tradeBatcher.on( + 'new batch', + this.candleManager.processTrades + ); + + this.candleManager.on( + 'candles', + this.pushCandles + ); + + Readable.call(this, {objectMode: true}); + + this.get(); +} + +var Readable = require('stream').Readable; +Market.prototype = Object.create(Readable.prototype, { + constructor: { value: Market } +}); + +Market.prototype._read = _.noop; + +Market.prototype.pushCandles = function(candles) { + _.each(candles, this.push); +} + +Market.prototype.get = function() { + this.fetcher.fetch(); +} + +Market.prototype.processTrades = function(trades) { + this.tradeBatcher.write(trades); + + if(this.done) + return log.info('Done importing!'); + + setTimeout(this.get, 1000); +} + +module.exports = Market; \ No newline at end of file diff --git a/core/markets/realtime.js b/core/markets/realtime.js new file mode 100644 index 000000000..b69cb8dd6 --- /dev/null +++ b/core/markets/realtime.js @@ -0,0 +1,22 @@ +var _ = require('lodash'); + +var util = require('../util'); +var dirs = util.dirs(); + +var config = util.getConfig(); + +var exchanges = require(dirs.gekko + 'exchanges'); +var exchange = _.find(exchanges, function(e) { + return e.slug === config.watch.exchange.toLowerCase(); +}); + +if(!exchange) + util.die(`Unsupported exchange: ${config.watch.exchange.toLowerCase()}`) + +var exchangeChecker = require(util.dirs().core + 'exchangeChecker'); + +var error = exchangeChecker.cantMonitor(config.watch); +if(error) + util.die(error, true); + +module.exports = require(dirs.budfox + 'budfox'); \ No newline at end of file diff --git a/core/pipeline.js b/core/pipeline.js new file mode 100644 index 000000000..8ffb7a8b6 --- /dev/null +++ b/core/pipeline.js @@ -0,0 +1,177 @@ +/* + + A pipeline implements a full Gekko Flow based on a config and + a mode. The mode is an abstraction that tells Gekko what market + to load (realtime, backtesting or importing) while making sure + all enabled plugins are actually supported by that market. + + Read more here: + @link https://github.com/askmike/gekko/blob/stable/docs/internals/architecture.md + +*/ + +var util = require('./util'); +var dirs = util.dirs(); + +var _ = require('lodash'); +var async = require('async'); + +var log = require(dirs.core + 'log'); + +var pipeline = (settings) => { + + var mode = settings.mode; + var config = settings.config; + + // prepare a GekkoStream + var GekkoStream = require(dirs.core + 'gekkoStream'); + + // all plugins + var plugins = []; + // all emitting plugins + var emitters = {}; + // all plugins interested in candles + var candleConsumers = []; + + // utility to check and load plugins. + var pluginHelper = require(dirs.core + 'pluginUtil'); + + // meta information about every plugin that tells Gekko + // something about every available plugin + var pluginParameters = require(dirs.gekko + 'plugins'); + + // Instantiate each enabled plugin + var loadPlugins = function(next) { + + // load all plugins + async.mapSeries( + pluginParameters, + pluginHelper.load, + function(error, _plugins) { + if(error) + return util.die(error, true); + + plugins = _.compact(_plugins); + next(); + } + ); + }; + + // Some plugins emit their own events, store + // a reference to those plugins. + var referenceEmitters = function(next) { + + _.each(plugins, function(plugin) { + if(plugin.meta.emits) + emitters[plugin.meta.slug] = plugin; + }); + + next(); + } + + // Subscribe all plugins to other emitting plugins + var subscribePlugins = function(next) { + var subscriptions = require(dirs.gekko + 'subscriptions'); + + // events broadcasted by plugins + var pluginSubscriptions = _.filter( + subscriptions, + function(sub) { + return sub.emitter !== 'market'; + } + ); + + // subscribe interested plugins to + // emitting plugins + _.each(plugins, function(plugin) { + _.each(pluginSubscriptions, function(sub) { + if(_.has(plugin, sub.handler)) { + + // if a plugin wants to listen + // to something disabled + if(!emitters[sub.emitter]) { + return log.warn([ + plugin.meta.name, + 'wanted to listen to the', + sub.emitter + ',', + 'however the', + sub.emitter, + 'is disabled.' + ].join(' ')); + } + + // attach handler + emitters[sub.emitter] + .on(sub.event, + plugin[ + sub.handler + ]) + } + + }); + }); + + // events broadcasted by the market + var marketSubscriptions = _.filter( + subscriptions, + {emitter: 'market'} + ); + + // subscribe plugins to the market + _.each(plugins, function(plugin) { + _.each(marketSubscriptions, function(sub) { + + // for now, only subscribe to candles + if(sub.event !== 'candle') + return; + + if(_.has(plugin, sub.handler)) + candleConsumers.push(plugin); + + }); + }); + + next(); + } + + // TODO: move this somewhere where it makes more sense + var prepareMarket = function(next) { + + if(mode === 'backtest' && config.backtest.daterange === 'scan') + require(dirs.core + 'prepareDateRange')(next); + else + next(); + } + + log.info('Setting up Gekko in', mode, 'mode'); + log.info(''); + + async.series( + [ + loadPlugins, + referenceEmitters, + subscribePlugins, + prepareMarket + ], + function() { + // load a market based on the mode + var Market = require(dirs.markets + mode); + + var market = new Market(config); + var gekko = new GekkoStream(candleConsumers); + + market + .pipe(gekko) + + // convert JS objects to JSON string + // .pipe(new require('stringify-stream')()) + // output to standard out + // .pipe(process.stdout); + + market.on('end', gekko.finalize); + } + ); + +} + +module.exports = pipeline; \ No newline at end of file diff --git a/core/pluginUtil.js b/core/pluginUtil.js new file mode 100644 index 000000000..1a92e3dca --- /dev/null +++ b/core/pluginUtil.js @@ -0,0 +1,119 @@ +var _ = require('lodash'); +var async = require('async'); + +var util = require(__dirname + '/util'); + +var log = require(util.dirs().core + 'log'); + +var config = util.getConfig(); +var pluginDir = util.dirs().plugins; +var gekkoMode = util.gekkoMode(); + +var pluginHelper = { + // Checks whether we can load a module + + // @param Object plugin + // plugin config object + // @return String + // error message if we can't + // use the module. + cannotLoad: function(plugin) { + + // verify plugin dependencies are installed + if(_.has(plugin, 'dependencies')) + var error = false; + + _.each(plugin.dependencies, function(dep) { + try { + var a = require(dep.module); + } + catch(e) { + log.error('ERROR LOADING DEPENDENCY', dep.module); + + if(!e.message) { + log.error(e); + util.die(); + } + + if(!e.message.startsWith('Cannot find module')) + return util.die(e); + + error = [ + 'The plugin', + plugin.slug, + 'expects the module', + dep.module, + 'to be installed.', + 'However it is not, install', + 'it by running: \n\n', + '\tnpm install', + dep.module + '@' + dep.version + ].join(' '); + } + }); + + return error; + }, + // loads a plugin + // + // @param Object plugin + // plugin config object + // @param Function next + // callback + load: function(plugin, next) { + + plugin.config = config[plugin.slug]; + + if(!plugin.config) + log.warn( + 'unable to find', + plugin.name, + 'in the config. Is your config up to date?' + ); + + if(!plugin.config || !plugin.config.enabled) + return next(); + + if(!_.contains(plugin.modes, gekkoMode)) { + log.warn( + 'The plugin', + plugin.name, + 'does not support the mode', + gekkoMode + '.', + 'It has been disabled.' + ) + return next(); + } + + log.info('Setting up:'); + log.info('\t', plugin.name); + log.info('\t', plugin.description); + + var cannotLoad = pluginHelper.cannotLoad(plugin); + if(cannotLoad) + return next(cannotLoad); + + if(plugin.path) + var Constructor = require(pluginDir + plugin.path(plugin.config)); + else + var Constructor = require(pluginDir + plugin.slug); + + if(plugin.async) { + var instance = new Constructor(util.defer(function(err) { + next(err, instance); + }), plugin); + instance.meta = plugin; + } else { + var instance = new Constructor(plugin); + instance.meta = plugin; + _.defer(function() { + next(null, instance); + }); + } + + if(!plugin.silent) + log.info('\n'); + } +} + +module.exports = pluginHelper; \ No newline at end of file diff --git a/core/prepareDateRange.js b/core/prepareDateRange.js new file mode 100644 index 000000000..ef927ba89 --- /dev/null +++ b/core/prepareDateRange.js @@ -0,0 +1,170 @@ +var _ = require('lodash'); +var moment = require('moment'); +var async = require('async'); + +var util = require('./util'); +var config = util.getConfig(); +var dirs = util.dirs(); +var adapter = config.adapters[config.backtest.adapter]; +var Reader = require(dirs.gekko + adapter.path + '/reader'); +var log = require(dirs.core + 'log'); +var prompt = require('prompt-lite'); + +var reader = new Reader(); + +var BATCH_SIZE = 60; // minutes + +// helper to store the evenutally detected +// daterange. +var setDateRange = function(from, to) { + config.backtest.daterange = { + from: moment.unix(from).utc().format(), + to: moment.unix(to).utc().format(), + }; + util.setConfig(config); +} + + +module.exports = function(done) { + + log.info('Scanning local history for backtestable dateranges.'); + + async.parallel({ + boundry: reader.getBoundry, + available: reader.countTotal + }, (err, res) => { + + var first = res.boundry.first; + var last = res.boundry.last; + + var optimal = (last - first) / 60; + + log.debug('Available', res.available); + log.debug('Optimal', optimal); + + // There is a candle for every minute + if(res.available === optimal + 1) { + log.info('Gekko is able to fully use the local history.'); + setDateRange(first, last); + return done(); + } + + // figure out where the gaps are.. + + var missing = optimal - res.available + 1; + + log.info(`The database has ${missing} candles missing, Figuring out which ones...`); + + var iterator = { + from: last - (BATCH_SIZE * 60), + to: last + } + + var batches = []; + + // loop through all candles we have + // in batches and track whether they + // are complete + async.whilst( + () => { + return iterator.from > first + }, + next => { + var from = iterator.from; + var to = iterator.to; + reader.count( + from, + iterator.to, + (err, count) => { + var complete = count === BATCH_SIZE + 1; + + if(complete) + batches.push({ + to: to, + from: from + }); + + next(); + } + ); + + iterator.from -= BATCH_SIZE * 60; + iterator.to -= BATCH_SIZE * 60; + }, + () => { + + if(!_.size(batches)) + util.die('Not enough data to work with (please manually set a valid `backtest.daterange`)..', true); + + // batches is now a list like + // [ {from: unix, to: unix } ] + + var ranges = [ batches.shift() ]; + + _.each(batches, batch => { + var curRange = _.last(ranges); + if(batch.to === curRange.from) + curRange.from = batch.from; + else + ranges.push( batch ); + }) + + // we have been counting chronologically reversed + // (backwards, from now into the past), flip definitions + ranges = ranges.reverse(); + _.map(ranges, r => { + return { + from: r.to, + to: r.from + } + }); + + + // ranges is now a list like + // [ {from: unix, to: unix } ] + // + // it contains all valid dataranges available for the + // end user. + + if(_.size(ranges) === 1) { + var r = _.first(ranges); + log.info('Gekko was able to find a single daterange in the locally stored history:'); + log.info('\t', 'from:', moment.unix(r.from).utc().format('YYYY-MM-DD HH:mm:ss')); + log.info('\t', 'to:', moment.unix(r.to).utc().format('YYYY-MM-DD HH:mm:ss')); + + setDateRange(r.from, r.to); + return done(); + } + + log.info( + 'Gekko detected multiple dateranges in the locally stored history.', + 'Please pick the daterange you are interested in testing:' + ); + + _.each(ranges, (range, i) => { + log.info('\t\t', `OPTION ${i + 1}:`); + log.info('\t', 'from:', moment.unix(range.from).utc().format('YYYY-MM-DD HH:mm:ss')); + log.info('\t', 'to:', moment.unix(range.to).utc().format('YYYY-MM-DD HH:mm:ss')); + }); + + prompt.get({name: 'option'}, (err, result) => { + + var option = parseInt(result.option); + if(option === NaN) + util.die('Not an option..', true); + + var range = ranges[option - 1]; + + if(!range) + util.die('Not an option..', true); + + setDateRange(range.from, range.to); + return done(); + }); + + } + ) + + + }); +} \ No newline at end of file diff --git a/core/talib.js b/core/talib.js new file mode 100644 index 000000000..88ba311d4 --- /dev/null +++ b/core/talib.js @@ -0,0 +1,921 @@ +var talib = require("talib"); + +var ifish = function (value) { + tmpvalue = 0.1 * (value - 50) + return result = (Math.exp(10*tmpvalue) - 1) / (Math.exp(10*tmpvalue) + 1) +}; + +var diff = function (x, y) { + return result = (( x - y ) / (Math.abs( x + y ) / 2)) * 100 +}; + +// create wrapper +var talibWrapper = function(params) { + return function(callback) { + return talib.execute(params, + function(result) { + if(result.error) + return callback(result.error); + + callback(null, result.result); + }); + }; +}; + +this.accbands = function(data, period) { + return talibWrapper({ + name: "ACCBANDS", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.ad = function(data, period) { + return talibWrapper({ + name: "AD", + high: data.high, + low: data.low, + close: data.close, + volume: data.volume, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.adosc = function(data, FastPeriod, SlowPeriod) { + return talibWrapper({ + name: "ADOSC", + high: data.high, + low: data.low, + close: data.close, + volume: data.volume, + startIdx: 0, + endIdx: data.high.length - 1, + optInFastPeriod: FastPeriod, + optInSlowPeriod: SlowPeriod + }); +}; + +this.adx = function(data, period) { + return talibWrapper({ + name: "ADX", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.adxr = function(data, period) { + return talibWrapper({ + name: "ADXR", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.apo = function(data, FastPeriod, SlowPeriod, MAType) { + return talibWrapper({ + name: "APO", + inReal: data.close, + startIdx: 0, + endIdx: data.length - 1, + optInFastPeriod: FastPeriod, + optInSlowPeriod: SlowPeriod, + optInMAType: MAType + }); +}; + +this.aroon = function(data, period) { + return talibWrapper({ + name: "AROON", + high: data.high, + low: data.low, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.aroonosc = function(data, period) { + return talibWrapper({ + name: "AROONOSC", + high: data.high, + low: data.low, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.atr = function(data, period) { + return talibWrapper({ + name: "ATR", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.avgprice = function(data, period) { + return talibWrapper({ + name: "AVGPRICE", + open: data.open, + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.open.length - 1, + optInTimePeriod: period + }); +}; + +this.bbands = function(data, period, NbDevUp, NbDevDn, MAType) { + return talibWrapper({ + name: "BBANDS", + inReal: data.close, + startIdx: 0, + endIdx: data.length - 1, + optInTimePeriod: period, + optInNbDevUp: NbDevUp, + optInNbDevDn: NbDevDn, + optInMAType: MAType + }); +}; + + +// ? +// this.beta = function(data_0, data_1, period) { +// return talibWrapper({ +// name: "BETA", +// inReal0: data_0, +// inReal1: data_1, +// startIdx: 0, +// endIdx: data_0.length - 1, +// optInTimePeriod: period +// }); +// }; + +this.bop = function(data) { + return talibWrapper({ + name: "BOP", + open: data.open, + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: high.length - 1 + }); +}; + +this.cci = function(data, period) { + return talibWrapper({ + name: "CCI", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.cmo = function(data, period) { + return talibWrapper({ + name: "CMO", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +// this.correl = function(data_0, data_1, period) { +// return talibWrapper({ +// name: "CORREL", +// inReal0: data_0, +// inReal1: data_1, +// startIdx: 0, +// endIdx: data_0.length - 1, +// optInTimePeriod: period +// }); +// }; + +this.dema = function(data, period) { + return talibWrapper({ + name: "DEMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.dx = function(data, period) { + return talibWrapper({ + name: "DX", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.ema = function(data, period) { + return talibWrapper({ + name: "EMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.ht_dcperiod = function(data) { + return talibWrapper({ + name: "HT_DCPERIOD", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.ht_dcphase = function(data) { + return talibWrapper({ + name: "HT_DCPHASE", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.ht_phasor = function(data) { + return talibWrapper({ + name: "HT_PHASOR", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.ht_sine = function(data) { + return talibWrapper({ + name: "HT_SINE", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.ht_trendline = function(data) { + return talibWrapper({ + name: "HT_TRENDLINE", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.ht_trendmode = function(data) { + return talibWrapper({ + name: "HT_TRENDMODE", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.imi = function(data, period) { + return talibWrapper({ + name: "IMI", + open: data.open, + close: data.close, + startIdx: 0, + endIdx: data.open.length - 1, + optInTimePeriod: period + }); +}; + +this.kama = function(data, period) { + return talibWrapper({ + name: "KAMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.linearreg = function(data, period) { + return talibWrapper({ + name: "LINEARREG", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.linearreg_angle = function(data, period) { + return talibWrapper({ + name: "LINEARREG_ANGLE", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.linearreg_intercept = function(data, period) { + return talibWrapper({ + name: "LINEARREG_INTERCEPT", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.linearreg_slope = function(data, period) { + return talibWrapper({ + name: "LINEARREG_SLOPE", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.ma = function(data, period, MAType) { + return talibWrapper({ + name: "MA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period, + optInMAType: MAType + }); +}; + +this.macd = function(data, FastPeriod, SlowPeriod, SignalPeriod) { + return talibWrapper({ + name: "MACD", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInFastPeriod: FastPeriod, + optInSlowPeriod: SlowPeriod, + optInSignalPeriod: SignalPeriod + }); +}; + +this.macdext = function(data, FastPeriod, FastMAType, SlowPeriod, SlowMAType, SignalPeriod, SignalMAType) { + return talibWrapper({ + name: "MACDEXT", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInFastPeriod: FastPeriod, + optInFastMAType: FastMAType, + optInSlowPeriod: SlowPeriod, + optInSlowMAType: SlowMAType, + optInSignalPeriod: SignalPeriod, + optInSignalMAType: SignalMAType + }); +}; + +this.macdfix = function(data, SignalPeriod) { + return talibWrapper({ + name: "MACDFIX", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInSignalPeriod: SignalPeriod + }); +}; + +this.mama = function(data, FastLimitPeriod, SlowLimitPeriod) { + return talibWrapper({ + name: "MAMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInFastLimit: FastLimitPeriod, + optInSlowLimit: SlowLimitPeriod + }); +}; + +this.mavp = function(data, periods, MinPeriod, MaxPeriod, MAType) { + return talibWrapper({ + name: "MAVP", + inReal: data.close, + inPeriods: periods, + startIdx: 0, + endIdx: data.close.length - 1, + optInMinPeriod: MinPeriod, + optInMaxPeriod: MaxPeriod, + optInMAType: MAType + }); +}; + +this.max = function(data, period) { + return talibWrapper({ + name: "MAX", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.maxindex = function(data, period) { + return talibWrapper({ + name: "MAXINDEX", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.medprice = function(data, period) { + return talibWrapper({ + name: "MEDPRICE", + high: data.high, + low: data.low, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.mfi = function(data, period) { + return talibWrapper({ + name: "MFI", + high: data.high, + low: data.low, + close: data.close, + volume: data.volume, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.midpoint = function(data, period) { + return talibWrapper({ + name: "MIDPOINT", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.midprice = function(data, period) { + return talibWrapper({ + name: "MIDPRICE", + high: data.high, + low: data.low, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.min = function(data, period) { + return talibWrapper({ + name: "MIN", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.minindex = function(data, period) { + return talibWrapper({ + name: "MININDEX", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.minmax = function(data, period) { + return talibWrapper({ + name: "MINMAX", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.minmaxindex = function(data, period) { + return talibWrapper({ + name: "MINMAXINDEX", + inReal: data.close, + startIdx: 0, + endIdx: data.length - 1, + optInTimePeriod: period + }); +}; + +this.minus_di = function(data, period) { + return talibWrapper({ + name: "MINUS_DI", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.minus_dm = function(high, low, period) { + return talibWrapper({ + name: "MINUS_DM", + high: data.high, + low: data.low, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.mom = function(data, period) { + return talibWrapper({ + name: "MOM", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.natr = function(data, period) { + return talibWrapper({ + name: "NATR", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.obv = function obv(data, volume) { + return talibWrapper({ + name: "OBV", + inReal: data.close, + volume: data.volume, + startIdx: 0, + endIdx: data.close.length - 1 + }); +}; + +this.plus_di = function(data, period) { + return talibWrapper({ + name: "PLUS_DI", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.plus_dm = function(high, low, period) { + return talibWrapper({ + name: "PLUS_DM", + high: data.high, + low: data.low, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.ppo = function(data, FastPeriod, SlowPeriod, MAType) { + return talibWrapper({ + name: "PPO", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInFastPeriod: FastPeriod, + optInSlowPeriod: SlowPeriod, + optInMAType: MAType + }); +}; + +this.roc = function(data, period) { + return talibWrapper({ + name: "ROC", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.rocp = function(data, period) { + return talibWrapper({ + name: "ROCP", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.rocr = function(data, period) { + return talibWrapper({ + name: "ROCR", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.rocr100 = function(data, period) { + return talibWrapper({ + name: "ROCR100", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.rsi = function(data, period) { + return talibWrapper({ + name: "RSI", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +// TODO: We don't have `accel` +// this.sar = function(high, low, accel, accelmax) { +// return talibWrapper({ +// name: "SAR", +// high: high, +// low: low, +// startIdx: 0, +// endIdx: data.high.length - 1, +// optInAcceleration: accel, +// optInMaximum: accelmax +// }); +// }; + +// this.sarext = function(high, low, StartValue, OffsetOnReverse, AccelerationInitLong, AccelerationLong, AccelerationMaxLong, AccelerationInitShort, AccelerationShort, AccelerationMaxShort) { +// return talibWrapper({ +// name: "SAREXT", +// high: high, +// low: low, +// startIdx: 0, +// endIdx: high.length - 1, +// optInStartValue: StartValue, +// optInOffsetOnReverse: OffsetOnReverse, +// optInAccelerationInitLong: AccelerationInitLong, +// optInAccelerationLong: AccelerationLong, +// optInAccelerationMaxLong: AccelerationMaxLong, +// optInAccelerationInitShort: AccelerationInitShort, +// optInAccelerationShort: AccelerationShort, +// optInAccelerationMaxShort: AccelerationMaxShort +// }); +// }; + +this.sma = function(data, period) { + return talibWrapper({ + name: "SMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.stddev = function(data, period, NbDev) { + return talibWrapper({ + name: "STDDEV", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period, + optInNbDev: NbDev + }); +}; + +this.stoch = function(data, fastK_period, slowK_period, slowK_MAType, slowD_period, slowD_MAType) { + return talibWrapper({ + name: "STOCH", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInFastK_Period: fastK_period, + optInSlowK_Period: slowK_period, + optInSlowK_MAType: slowK_MAType, + optInSlowD_Period: slowD_period, + optInSlowD_MAType: slowD_MAType + }); +}; + +this.stochf = function(data, fastK_period, fastD_period, fastD_MAType) { + return talibWrapper({ + name: "STOCHF", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInFastK_Period: fastK_period, + optInFastD_Period: fastD_period, + optInFastD_MAType: fastD_MAType + }); +}; + +this.stochrsi = function(data, period, fastK_period, fastD_period, fastD_MAType) { + return talibWrapper({ + name: "STOCHRSI", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period, + optInFastK_Period: fastK_period, + optInFastD_Period: fastD_period, + optInFastD_MAType: fastD_MAType + }); +}; + +this.sum = function(data, period) { + return talibWrapper({ + name: "SUM", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.t3 = function(data, period, vfactor) { + return talibWrapper({ + name: "T3", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period, + optInVFactor: vfactor + }); +}; + +this.tema = function(data, period) { + return talibWrapper({ + name: "TEMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.trange = function(data, period) { + return talibWrapper({ + name: "TRANGE", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.trima = function(data, period) { + return talibWrapper({ + name: "TRIMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.trix = function(data, period) { + return talibWrapper({ + name: "TRIX", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.tsf = function(data, period) { + return talibWrapper({ + name: "TSF", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +this.typprice = function(data, period) { + return talibWrapper({ + name: "TYPPRICE", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.ultosc = function(data, Period1, Period2, Period3) { + return talibWrapper({ + name: "ULTOSC", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod1: Period1, + optInTimePeriod2: Period2, + optInTimePeriod3: Period3 + }); +}; + +this.variance = function(data, period, NbVar) { + return talibWrapper({ + name: "VAR", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period, + optInNbDev: NbVar + }); +}; + +this.wclprice = function(data, period) { + return talibWrapper({ + name: "WCLPRICE", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.willr = function(data, period) { + return talibWrapper({ + name: "WILLR", + high: data.high, + low: data.low, + close: data.close, + startIdx: 0, + endIdx: data.high.length - 1, + optInTimePeriod: period + }); +}; + +this.wma = function(data, period) { + return talibWrapper({ + name: "WMA", + inReal: data.close, + startIdx: 0, + endIdx: data.close.length - 1, + optInTimePeriod: period + }); +}; + +module.exports = exports; \ No newline at end of file diff --git a/core/util.js b/core/util.js index cbbd303d2..b8c9bae43 100644 --- a/core/util.js +++ b/core/util.js @@ -3,10 +3,15 @@ var _ = require('lodash'); var path = require('path'); var fs = require('fs'); var semver = require('semver'); +var program = require('commander'); var _config = false; var _package = false; var _nodeVersion = false; +var _gekkoMode = false; +var _gekkoEnv = false; + +var _args = false; // helper functions var util = { @@ -14,7 +19,11 @@ var util = { if(_config) return _config; - var configFile = path.resolve(util.getArgument('config') || __dirname + '/../config.js'); + var configFile = path.resolve(program.config || util.dirs().gekko + 'config.js'); + + if(!fs.existsSync(configFile)) + util.die('Cannot find a config file.'); + _config = require(configFile); _config.resolvedLocation = configFile; return _config; @@ -36,6 +45,7 @@ var util = { if(_package) return _package; + _package = JSON.parse( fs.readFileSync(__dirname + '/../package.json', 'utf8') ); return _package; }, @@ -46,20 +56,6 @@ var util = { var required = util.getRequiredNodeVersion(); return semver.satisfies(process.version, required); }, - getArgument: function(argument) { - var ret; - _.each(process.argv, function(arg) { - // check if it's a configurable - var pos = arg.indexOf(argument + '='); - if(pos !== -1) - ret = arg.substr(argument.length + 1); - // check if it's a toggle - pos = arg.indexOf('-' + argument); - if(pos !== -1 && !ret) - ret = true; - }); - return ret; - }, // check if two moments are corresponding // to the same time equals: function(a, b) { @@ -84,36 +80,6 @@ var util = { var total = _.reduce(list, function(m, n) { return m + n }, 0); return total / list.length; }, - // calculate the average trade price out of a sample of trades. - // The sample consists of all trades that happened after the treshold. - calculatePriceSince: function(treshold, trades) { - var sample = []; - _.every(trades, function(trade) { - if(moment.unix(trade.date) < treshold) - return false; - - var price = parseFloat(trade.price); - sample.push(price); - return true; - }); - - return util.average(sample); - }, - // calculate the average trade price out of a sample of trades. - // The sample consists of all trades that happened before the treshold. - calculatePriceTill: function(treshold, trades) { - var sample = []; - _.every(trades, function(trade) { - if(moment.unix(trade.date) > treshold) - return false; - - var price = parseFloat(trade.price); - sample.push(price); - return true; - }); - - return util.average(sample); - }, calculateTimespan: function(a, b) { if(a < b) return b.diff(a); @@ -127,21 +93,95 @@ var util = { } }, logVersion: function() { - console.log('Gekko version:', 'v' + util.getVersion()); - console.log('Nodejs version:', process.version); + return `Gekko version: v${util.getVersion()}` + + `\nNodejs version: ${process.version}`; }, - die: function(m) { + die: function(m, soft) { if(m) { - console.log('\n\nGekko encountered an error and can\'t continue'); - console.log('\nMeta debug info:\n'); - util.logVersion(); - console.log('\nError:\n'); - console.log(m, '\n\n'); + if(soft) { + console.log('\n', m, '\n\n'); + } else { + console.log('\n\nGekko encountered an error and can\'t continue'); + console.log('\nError:\n'); + console.log(m, '\n\n'); + console.log('\nMeta debug info:\n'); + console.log(util.logVersion()); + console.log(''); + } + } + process.exit(0); + }, + dirs: function() { + var ROOT = __dirname + '/../'; + + return { + gekko: ROOT, + core: ROOT + 'core/', + markets: ROOT + 'core/markets/', + exchanges: ROOT + 'exchanges/', + plugins: ROOT + 'plugins/', + methods: ROOT + 'methods/', + budfox: ROOT + 'core/budfox/', + importers: ROOT + 'importers/exchanges/' } - process.kill(); + }, + inherit: function(dest, source) { + require('util').inherits( + dest, + source + ); + }, + makeEventEmitter: function(dest) { + util.inherit(dest, require('events').EventEmitter); + }, + setGekkoMode: function(mode) { + _gekkoMode = mode; + }, + gekkoMode: function() { + if(_gekkoMode) + return _gekkoMode; + + if(program['import']) + return 'importer'; + else if(program.backtest) + return 'backtest'; + else + return 'realtime'; + }, + gekkoModes: function() { + return [ + 'importer', + 'backtest', + 'realtime' + ] + }, + setGekkoEnv: function(env) { + _gekkoEnv = env; + }, + gekkoEnv: function() { + return _gekkoEnv || 'standalone'; } } +// NOTE: those options are only used +// in stand alone mode +program + .version(util.logVersion()) + .option('-c, --config ', 'Config file') + .option('-b, --backtest', 'backtesting mode') + .option('-i, --import', 'importer mode') + .parse(process.argv); + var config = util.getConfig(); +// make sure the current node version is recent enough +if(!util.recentNode()) + util.die([ + 'Your local version of Node.js is too old. ', + 'You have ', + process.version, + ' and you need atleast ', + util.getRequiredNodeVersion() + ].join(''), true); + module.exports = util; \ No newline at end of file diff --git a/docs/Advanced_features.md b/docs/Advanced_features.md index e4f569850..00459887e 100644 --- a/docs/Advanced_features.md +++ b/docs/Advanced_features.md @@ -65,15 +65,15 @@ You can also use this feature to do a realtime study on what different EMA setti To specify a different config file, you can use the following command line argument: - node gekko config=config/user/alternative-config + node gekko --config config/user/alternative-config or a relative path: - node gekko config=../../alternative-config + node gekko --config ../../alternative-config or a static path: - node gekko config=home/gekko/config/user/alternative-config + node gekko --config home/gekko/config/user/alternative-config # Helper files @@ -89,4 +89,4 @@ Use `gekko_log_grab.sh` to start tailing the log instead of via screen. The syn # Building on top of the Gekko platform -Gekko is built around an event emitting architecture. Those events glue core together and provide an API for [additional plugins](https://github.com/askmike/gekko/blob/master/docs/internals/plugins.md). On default the events stay within a single Gekko (a single nodejs process), though using the [Redis Beacon plugin](https://github.com/askmike/gekko/blob/master/docs/internals/plugins.md#redis-beacon) all events can be broadcasted on the Redis Pub/Sub system. This makes it a breeze to integrate Gekko in your own applications (which can live outside the Gekko process, outside any nodejs environment and across a network / cluster on different hosts - use your imagination). +Gekko is built around an event emitting architecture. Those events glue core together and provide an API for [additional plugins](https://github.com/askmike/gekko/blob/stable/docs/internals/plugins.md). On default the events stay within a single Gekko (a single nodejs process), though using the [Redis Beacon plugin](https://github.com/askmike/gekko/blob/stable/docs/internals/plugins.md#redis-beacon) all events can be broadcasted on the Redis Pub/Sub system. This makes it a breeze to integrate Gekko in your own applications (which can live outside the Gekko process, outside any nodejs environment and across a network / cluster on different hosts - use your imagination). diff --git a/docs/Backtesting.md b/docs/Backtesting.md index e7735c8a8..02640e739 100644 --- a/docs/Backtesting.md +++ b/docs/Backtesting.md @@ -1,52 +1,68 @@ -**These are old docs referrring to the old stable master branch on Gekko. The backtester is broken in this branch** - # Backtesting with Gekko -**Note that this functionality should only be used for testing purposes at this moment as it's in early development stage** +Gekko is able to backtest strategies against historical data. + +## Setup + +For backtesting you should [enable and configure](./Plugins.md) the following plugins: -After you configured and run the backtester Gekko will output the results like so: + - trading advisor (to run your strategy) + - profit simulator (to calculate how succesfull the strategy would have been) - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) start time: 2013-04-24 07:00:00 - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) end time: 2013-05-23 16:00:00 - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) timespan: 29 days +Besides that, make sure to configure `config.watch`. - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) start price: 121.6 - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) end price: 125.44 - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) Buy and Hold profit: 3.158% +## Historical data - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) amount of trades: 15 - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) original simulated balance: 245.404 USD - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) current simulated balance: 281.819 USD - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) simulated profit: 36.415 USD (14.839%) - 2013-06-30 13:25:30 (INFO): (PROFIT REPORT) simulated yearly profit: 447.030 USD (182.161%) +Gekko requires historical data to backtest strategies against. The easiest way to get this is to let Gekko import historical data, however this is not supported by a lot of exchanges (see [here](https://github.com/askmike/gekko#supported-exchanges)). The second easiest and most universal way is to run Gekko on real markets with the plugin sqliteWriter enabled (this will cause Gekko to store realtime data on disk). -## Preparing Gekko +## Configure -You can configure Gekko to test the current EMA strategy on historical data. To do this you need candle data in CSV format. On [this webpage](https://bitcointalk.org/index.php?topic=239815.0) you can downloaded precalculated candles from Mt. Gox or you can calculate your own using the script provided in the link. Alternatively you can supply your own candles, the only requirement is that the csv file has candles ordered like this: `timestamp,open,high,low,close`. +In your config set the `backtest.daterange` to `scan`. This will force Gekko to scan the local database to figure out what dataranges are available. If you already know exactly what daterange you would like to backtest against, you can set the `backtest.daterange` directly. -## Configuring Gekko +## Run -Once you have the csv file with candles you can configure Gekko for backtesting: in [config.js](https://github.com/askmike/gekko/blob/master/config.js) in the advanced zone you need to the backtesting part like so: + node gekko --backtest - config.backtest = { - candleFile: 'candles.csv', // the candles file - from: 0, // optional start timestamp - to: 0 // optional end timestamp - } +The result will be something like this: -Once configured Gekko will run the backtest instead of watching the live market. It wil use the following configuration items: + 2016-06-11 08:53:20 (INFO): Gekko v0.2.1 started + 2016-06-11 08:53:20 (INFO): I'm gonna make you rich, Bud Fox. -* Everything under `backtest`. -* Everything under `profitCalculator`. -* Everything under `EMA` with the exception of interval, as this will be determined by the candles file. + 2016-06-11 08:53:20 (INFO): Setting up Gekko in backtest mode + 2016-06-11 08:53:20 (INFO): + 2016-06-11 08:53:20 (WARN): The plugin SQLite Datastore does not support the mode backtest. It has been disabled. + 2016-06-11 08:53:20 (INFO): Setting up: + 2016-06-11 08:53:20 (INFO): Trading Advisor + 2016-06-11 08:53:20 (INFO): Calculate trading advice + 2016-06-11 08:53:20 (INFO): Using the trading method: DEMA + 2016-06-11 08:53:20 (INFO): -## Running the backtester + 2016-06-11 08:53:20 (INFO): Setting up: + 2016-06-11 08:53:20 (INFO): Profit Simulator + 2016-06-11 08:53:20 (INFO): Paper trader that logs fake profits. + 2016-06-11 08:53:20 (INFO): -Instead of running the paper / live trading Gekko using `node gekko`, you can start the backtester by running: + 2016-06-11 08:58:20 (INFO): Profit simulator got advice to long @ 2016-05-30 04:37:00, buying 1.1880062 BTC + 2016-06-11 08:58:21 (INFO): Profit simulator got advice to short @ 2016-05-31 21:37:00, selling 1.1880062 BTC + 2016-06-11 08:58:21 (INFO): Profit simulator got advice to long @ 2016-06-01 12:37:00, buying 1.14506098 BTC + 2016-06-11 08:58:21 (INFO): Profit simulator got advice to short @ 2016-06-02 14:57:00, selling 1.14506098 BTC + 2016-06-11 08:58:21 (INFO): Profit simulator got advice to long @ 2016-06-02 23:37:00, buying 1.11711818 BTC + 2016-06-11 08:58:21 (INFO): Profit simulator got advice to short @ 2016-06-05 12:57:00, selling 1.11711818 BTC + 2016-06-11 08:58:21 (INFO): Profit simulator got advice to long @ 2016-06-06 02:37:00, buying 1.08456953 BTC + 2016-06-11 08:58:22 (INFO): Profit simulator got advice to short @ 2016-06-07 17:17:00, selling 1.08456953 BTC + 2016-06-11 08:58:22 (INFO): Profit simulator got advice to long @ 2016-06-08 13:17:00, buying 1.05481755 BTC + 2016-06-11 08:58:22 (INFO): Profit simulator got advice to short @ 2016-06-09 14:17:00, selling 1.05481755 BTC - node gekko-backtest + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) start time: 2016-05-29 23:34:00 + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) end time: 2016-06-10 08:56:00 + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) timespan: 11 days days -## Notes + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) start price: 516.19 + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) end price: 578.97 + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) Buy and Hold profit: 12.162189999999995% -* Use the backtesting feature only for testing until the code is stable. -* When there are missing candles Gekko will act as if the whole duration of the missing candle never happened. \ No newline at end of file + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) amount of trades: 10 + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) original simulated balance: 616.19000 USD + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) current simulated balance: 602.59867 USD + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) simulated profit: -13.59133 USD (-2.20570%) + 2016-06-11 08:53:22 (INFO): (PROFIT REPORT) simulated yearly profit: -435.53244 USD (-70.68152%) \ No newline at end of file diff --git a/docs/Configuring_gekko.md b/docs/Configuring_gekko.md index 15fe0aa54..ece8f4d7f 100644 --- a/docs/Configuring_gekko.md +++ b/docs/Configuring_gekko.md @@ -1,21 +1,17 @@ # Configuring Gekko -*(Note that backtesting is not discussed as it is not implemented yet)*. +*(For backtesting, see this [doc](./Backtesting.md))*. -Configuring Gekko consists of three parts: +Configuring Gekko consists of two parts: -- [Watching a realtime market](#watching-a-realtime-market) -- [Automate trading advice](#automate-trading-advice) +- [Watching a market](#watching-a-market) - [Enabling plugins](#enabling-plugins) -## Watching a realtime market +First of all create a copy of `sample-config.js` and name it `config.js`. From now on all changes go to `config.js` file. -It all starts with deciding which market you want Gekko to monitor, Gekko watches a single market and all advice and price information is based on this market. A market is a currency/asset pair on a supported exchange. Examples are: +## Watching a market -- USD/BTC on Mt. Gox -- USD/BTC on BTC-e -- BTC/LTC on BTC-e -- BTC/GHS on CEX.io +It all starts with deciding which market you want Gekko to monitor, Gekko watches a single market and all advice and price information is based on this market. A market is a currency/asset pair on a supported exchange. The supported exchanges can be found [here](./supported_exchanges.md), and the supported assets and currencies for each exchange can be seen [here](https://github.com/askmike/gekko/blob/stable/exchanges.js). ### Configuring an exchange @@ -23,388 +19,20 @@ Open up the config.js file inside the Gekko directory with a text editor and sea // Monitor the live market config.watch = { - enabled: true, - exchange: 'btce', // 'MtGox', 'BTCe', 'Bitstamp' or 'cexio' + exchange: 'btce', currency: 'USD', asset: 'BTC' } -- enabled tells gekko it should monitor a market, ~~disable for backtesting.~~ -- exchange tells Gekko what exchange this market is on, check in supported markets what exchanges are supported. -- currency tells Gekko what currency the market you want has*. -- asset tells Gekko what currency the market you want has*. - -*Even though Bitcoin is a currency Gekko treats is like an asset when you are trading USD/BTC. +- `enabled` tells Gekko what market to monitor. +- `exchange` tells Gekko what exchange this market is on, check in supported markets what exchanges are supported. +- `currency` tells Gekko what currency the market you want has. +- `asset` tells Gekko what currency the market you want has. ### Supported markets -* Mt. Gox: - currencies: USD, EUR, GBP, AUD, CAD, CHF, CNY, DKK, HKD, PLN, RUB, SGD, THB - assets: BTC - markets: USD/BTC, EUR/BTC, GBP/BTC, AUD/BTC, CAD/BTC, CHF/BTC, CNY/BTC, DKK/BTC, HKD/BTC, PLN/BTC, RUB/BTC, SGD/BTC, THB/BTC. - -* BTC-e: - currencies: USD, EUR, RUR, BTC - assets: BTC, LTC, NMC, NVC, USD, EUR, TRC, PPC, FTC, XPM - markets: USD/BTC, RUR/BTC, EUR/BTC, BTC/LTC, USD/LTC, RUR/LTC, EUR/LTC, BTC/NMC, USD/NMC, BTC/NVC, USD/NVC, RUR/USD, USD/EUR, BTC/TRC, BTC/PPC, USD/PPC, BTC/FTC, BTC/XPM. - -* Bitstamp: - currencies: USD - assets: BTC - markets: USD/BTC - -* CEX.io: - currencies: BTC - assets: GHS - markets: BTC/GHS - -* Kraken: - currencies: XRP, EUR, KRW, USD, LTC, XVN - assets: LTC, NMC, XBT, XVN, EUR, KRW, USD - markets: XRP/LTC, EUR/LTC, KRW/LTC, USD/LTC, XRP/NMC, EUR/NMC, KRW/NMC, USD/NMC, LTC/XBT, NMC/XBT, XRP/XBT, XVN/XBT, EUR/XBT, KRW/XBT, USD/XBT, XRP/XVN, XRP/EUR, XVN/EUR, XRP/KRW, XRP/USD, XVN/USD. - -## Automate trading advice - -If you want Gekko to provide automated trading advice you need to configure this here. Note that this has unrelated to automatic trading which is a plugin that creates order based on this advice. (So you need to calculate advice if you want to automate trading.) - -Gekko supports a number of technical analysis indicators, currently it supports trading methods for the indicators: - -- DEMA -- MACD -- PPO -- RSI - -Open up the config.js file again and configure at this part: - - config.tradingAdvisor = { - enabled: true, - method: 'DEMA', - candleSize: 5, - historySize: 20 - } - -- enabeld tells gekko it should calculate advice. -- Method tells gekko what indicator it should calculate. -- candleSize tells Gekko the size of the candles (in minutes) you want to calculate the indicator over. If you want MACD advice over hourly candles set this to 60. -- historySize tells gekko how much historical candles Gekko needs before it can calculate the initial advise. This is due to the fact that all current indicators need to have initial data. - -### IMPORTANT NOTES - -- If you have 60 minutes candles that does not mean you will get advice every 60 minutes. You only get advice if the configured indicator suggests to take a new position. -- All the advice is based on the configured indicators you gave to Gekko. The advice is not me or Gekko saying you should take a certain position in the market. **It is the result of automatically running the indicators you configured on the live market.** -- Gekko calculates the advice silently, but you can turn on plugins that do something with this advice. - -### DEMA - -This method uses `Exponential Moving Average crossovers` to determine the current trend the -market is in. Using this information it will suggest to ride the trend. Note that this is -not MACD because it just checks whether the longEMA and shortEMA are [threshold]% removed -from eachother. - -This method is fairly popular in bitcoin trading due to Bitcointalk user Goomboo. Read more about this method in [his topic](https://bitcointalk.org/index.php?topic=60501.0) - -You can configure these parameters for DEMA in config.js: - - config.DEMA = { - // EMA weight (α) - // the higher the weight, the more smooth (and delayed) the line - short: 10, - long: 21, - // amount of candles to remember and base initial EMAs on - // the difference between the EMAs (to act as triggers) - thresholds: { - down: -0.025, - up: 0.025 - } - }; - -- short is the short EMA that moves closer to the real market (including noise) -- long is the long EMA that lags behind the market more but is also more resistant to noise. -- the down treshold and the up treshold tell Gekko how big the difference in the lines needs to be for it to be considered a trend. If you set these to 0 each line cross would trigger new advice. - -### MACD - -This method is similar to DEMA but goes a little further by comparing the difference by an EMA of itself. Read more about it [here](http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_average_conve). - -You can configure these parameters for MACD in config.js: - - // MACD settings: - config.MACD = { - // EMA weight (α) - // the higher the weight, the more smooth (and delayed) the line - short: 10, - long: 21, - signal: 9, - // the difference between the EMAs (to act as triggers) - thresholds: { - down: -0.025, - up: 0.025, - // How many candle intervals should a trend persist - // before we consider it real? - persistence: 1 - } - }; - -- short is the short EMA that moves closer to the real market (including noise) -- long is the long EMA that lags behind the market more but is also more resistant to noise. -- signal is the EMA weight calculated over the difference from short/long. -- the down treshold and the up treshold tell Gekko how big the difference in the lines needs to be for it to be considered a trend. If you set these to 0 each line cross would trigger new advice. -- persistence tells Gekko how long the thresholds needs to be met until Gekko considers the trend to be valid. - -### PPO - -Very similar to MACD but also a little different, read more [here](http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:price_oscillators_pp). - - // PPO settings: - config.PPO = { - // EMA weight (α) - // the higher the weight, the more smooth (and delayed) the line - short: 12, - long: 26, - signal: 9, - // the difference between the EMAs (to act as triggers) - thresholds: { - down: -0.025, - up: 0.025, - // How many candle intervals should a trend persist - // before we consider it real? - persistence: 2 - } - }; - -- short is the short EMA that moves closer to the real market (including noise) -- long is the long EMA that lags behind the market more but is also more resistant to noise. -- signal is the EMA weight calculated over the difference from short/long. -- the down treshold and the up treshold tell Gekko how big the difference in the lines needs to be for it to be considered a trend. If you set these to 0 each line cross would trigger new advice. -- persistence tells Gekko how long the thresholds needs to be met until Gekko considers the trend to be valid. - -### RSI - -The Relative Strength Index is a momentum oscillator that measures the speed and change of price movements. Read more about it [here](http://stockcharts.com/help/doku.php?id=chart_school:technical_indicators:relative_strength_in). Configure it like so: - - // RSI settings: - config.RSI = { - interval: 14, - thresholds: { - low: 30, - high: 70, - // How many candle intervals should a trend persist - // before we consider it real? - persistence: 1 - } - }; - -- The interval is the amount of periods the RSI should use. -- The thresholds determine what level of RSI would trigger an up or downtrend. -- persistence tells Gekko how long the thresholds needs to be met until Gekko considers the trend to be valid. - -## Enabling plugins - -Gekko currently has a couple plugins: - -- trader -- advice logger -- profit simulator -- Mailer -- IRC bot -- Campfire bot -- Redis beacon - -### NOTES - -When you turn on a plugin for the first time it might be that you need to install some additional dependencies. Copy paste what Gekko tells you! - -### Trader - -This plugin automatically creates orders based on the advice on the market it is watching. This turns Gekko into an automated trading bot. - -Before Gekko can automatically trade you need to create API keys so that Gekko has the rights to create orders on your behalf, the rights Gekko needs are (naming differs per exchange): get info, get balance/portfolio, get open orders, get fee, buy, sell and cancel order. For all exchanges you need the API key and the API secret, for both Bitstamp and CEX.io you also need your username (which is a number at Bitstamp). - -Configure it like this: - - config.trader = { - enabled: true, - key: 'your-api-key', - secret: 'your-api-secret', - username: 'your-username' // your username, only fill in when using bitstamp or cexio - } - -- enabled indicates whether this is on or off. -- key is your API key. -- secret is your API secret. -- username is the username (only required for CEX.io and Bitstamp). +A list of all supported exchanges and their markets can be found [here](https://github.com/askmike/gekko/blob/stable/exchanges.js). -#### Special note for CEX.io users - -When you have GHS on cexio you will get payouts in the form of mining rewards, however those rewards are always in the form of the currency (BTC). For as long as the advice is to go long (buy GHS) Gekko will check every five minutes if new payout has been earned, if it is Gekko will reinvest this into GHS. - -### Advice logger - -The advice logger is a small plugin that logs new advice calculated by Gekko as soon as there is any. Go to the config and configure it like this: - - config.adviceLogger = { - enabled: true - } - -- enabled indicates whether this is on or off. - -The advice logged advice will look something like this in the terminal: - - 2014-01-15 14:31:44 (INFO): We have new trading advice! - 2014-01-15 14:31:44 (INFO): Position to take: long - 2014-01-15 14:31:44 (INFO): Market price: 5.96 - 2014-01-15 14:31:44 (INFO): Based on market time: 2014-01-15 14:31:01 - -### profit simulator (paper trader) - -The calculator listens to Gekko's advice and on a sell it will swap all (simulated) currency into (simulated) assets at the current price. On a buy it will be the other way around. - -Go to the config and configure it like this: - - // do you want Gekko to calculate the profit of its own advice? - config.profitSimulator = { - enabled: true, - // report the profit in the currency or the asset? - reportInCurrency: true, - // start balance, on what the current balance is compared with - simulationBalance: { - // these are in the unit types configured in the watcher. - asset: 1, - currency: 100, - }, - // only want report after a sell? set to `false`. - verbose: false, - // how much fee in % does each trade cost? - fee: 0.6, - // how much slippage should Gekko assume per trade? - slippage: 0.1 - } - -- enabled indicates whether this is on or off. -- reportInCurrency tells Gekko whether it should report in asset or in the currency. -- simulationBalance tells Gekko with what balance it should start. -- verbose specifies how often Gekko should log the results (false is after every trade, true is after every candle). -- fee is the exchange fee (in %) Gekko should take into considarion when simulating orders. -- slippage is the costs in (in %) associated with not being able to buy / sell at market price.* - -*If you are trading a lot and you are buying 100% currency you might not get it all at market price and you have to walk the book in order to take that position. Also note that Gekko uses the candle close price and is unaware of the top asks bids, also take this into account. It is important that you set this number correctly or the resulted calculated profit be very wrong. Read more information [here](http://www.investopedia.com/terms/s/slippage.asp). Take these into consideration when setting a slippage: - -- How much spread is there normally on this market? -- How thick is the top of the book normally? -- How volatile is this market (the more volatility the bigger the change you will not get the price you expected)? - -The output will be something like: - - 2013-06-02 18:21:15 (INFO): ADVICE is to SHORT @ 117.465 (0.132) - 2013-06-02 18:21:15 (INFO): (PROFIT REPORT) original balance: 207.465 USD - 2013-06-02 18:21:15 (INFO): (PROFIT REPORT) current balance: 217.465 USD - 2013-06-02 18:21:15 (INFO): (PROFIT REPORT) profit: 10.000 USD (4.820%) - -#### Special note for CEX.io users - -At CEX.io the asset is bound to devalue over time, however investers are compensated with mining rewards. Because -the size of this reward depends on factors outside the market **this reward is not taken into consideration when simulating the profit.** - -### Mailer - -Mailer will automatically email you whenever Gekko has a new advice. - - // want Gekko to send a mail on buy or sell advice? - config.mailer = { - enabled: false, // Send Emails if true, false to turn off - sendMailOnStart: true, // Send 'Gekko starting' message if true, not if false - - Email: 'me@gmail.com', // Your GMail address - - // You don't have to set your password here, if you leave it blank we will ask it - // when Gekko's starts. - // - // NOTE: Gekko is an open source project < https://github.com/askmike/gekko >, - // make sure you looked at the code or trust the maintainer of this bot when you - // fill in your email and password. - // - // WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT - // guarantuee that your email address & password are safe! - - password: '', // Your GMail Password - if not supplied Gekko will prompt on startup. - tag: '[GEKKO] ', // Prefix all EMail subject lines with this - } - -- enabled indicates whether this is on or off. -- sendMailOnStart will email you right away after Gekko started, you can also use this if you are automatically restarting Gekko to see crash behaviour. -- Email is your email address from which Gekko will send emails (to the same address). -- password is the email password: Gekko needs to login to your account to send emails to you: - - > You don't have to set your password here, if you leave it blank we will ask it - > when Gekko's starts. - > - > NOTE: Gekko is an open source project < https://github.com/askmike/gekko >, - > make sure you looked at the code or trust the maintainer of this bot when you - > fill in your email and password. - > - > WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT - > guarantuee that your email address & password are safe! - -- tag is some text that Gekko will put in all subject lines so you can easily group all advices together. - -### IRC bot - -IRC bot is a small plugin that connects Gekko to an IRC channel and lets users interact with it using basic commands. - - config.ircbot = { - enabled: false, - emitUpdats: false, - channel: '#your-channel', - server: 'irc.freenode.net', - botName: 'gekkobot' - } - -- enabled indicates whether this is on or off. -- emitUpdates tells Gekko that whenever there is a new advice it should broadcast this in the channel. -- channel is the IRC channel the bot will connect to. -- server is the IRC server. -- botName is the username Gekko will use. - -### Campfire bot - -Campfire bot is a small plugin that connects Gekko to a Campfire room and lets users interact with it using basic commands. - - config.campfire = { - enabled: false, - emitUpdats: false, - nickname: 'Gordon', - roomId: 673783, - apiKey: 'e3b0c44298fc1c149afbf4c8996', - account: 'your-subdomain' - } - -- enabled indicates whether this is on or off. -- emitUpdates tells Gekko that whenever there is a new advice it should broadcast this in the room. -- roomId is the ID of the Campfire room the bot will connect to. -- apiKey is the API key for the Campfire user Gekko will connect using. -- account is the subdomain for the account that the room belongs to. - -### Redis beacon - -This is an advanced plugin only for programmers! If you are interested in this read more [here](https://github.com/askmike/gekko/blob/master/docs/internals/plugins.md#redis-beacon). - - config.redisBeacon = { - enabled: false, - port: 6379, // redis default - host: '127.0.0.1', // localhost - // On default Gekko broadcasts - // events in the channel with - // the name of the event, set - // an optional prefix to the - // channel name. - channelPrefix: '', - broadcast: [ - 'small candle' - ] - } +### Enable plugins -- enabled indicates whether this is on or off. -- port is the port redis is running on. -- host is the redis host. -- channelPrefix a string that Gekko will prefix all candles with. -- broadcast is a list of all events you want Gekko to publish. +Everything Gekko does with market data (trading, emailing, etc.) is handled to plugins. The [plugin documentation](./Plugins.md) explains all plugins gekko has. \ No newline at end of file diff --git a/docs/Importing.md b/docs/Importing.md new file mode 100644 index 000000000..2a55dc2fe --- /dev/null +++ b/docs/Importing.md @@ -0,0 +1,39 @@ +# Importing + +If you want to use Gekko to [backtest against historical data](./Backtesting.md), you most likely need some historical data to test against. Gekko comes with the functionality to automatically import historical data from some exchanges. However, only a few exchanges support this. You can find out with which exchanges Gekko is able to do this [here](https://github.com/askmike/gekko#supported-exchanges). + +## Setup + +For importing you should [enable and configure](./Plugins.md) the following plugin: + + - candleWriter (to store the imported data in a database) + +Besides that, make sure to configure `config.watch` properly. + +## Configure + +In your config set the `importer.daterange` properties to the daterange you would like to import. + +## Run + + node gekko --import + +The result will be something like this: + + 2016-06-26 09:12:16 (INFO): Gekko v0.2.2 started + 2016-06-26 09:12:16 (INFO): I'm gonna make you rich, Bud Fox. + + 2016-06-26 09:12:17 (INFO): Setting up Gekko in importer mode + 2016-06-26 09:12:17 (INFO): + 2016-06-26 09:12:17 (INFO): Setting up: + 2016-06-26 09:12:17 (INFO): Candle writer + 2016-06-26 09:12:17 (INFO): Store candles in a database + 2016-06-26 09:12:17 (INFO): + + 2016-06-26 09:12:17 (WARN): The plugin Trading Advisor does not support the mode importer. It has been disabled. + 2016-06-26 09:12:17 (WARN): The plugin Advice logger does not support the mode importer. It has been disabled. + 2016-06-26 09:12:17 (WARN): The plugin Profit Simulator does not support the mode importer. It has been disabled. + 2016-06-26 09:12:18 (DEBUG): Processing 798 new trades. + 2016-06-26 09:12:18 (DEBUG): From 2015-09-09 12:00:04 UTC to 2015-09-09 13:58:55 UTC. (2 hours) + 2016-06-26 09:12:20 (DEBUG): Processing 211 new trades. + (...) diff --git a/docs/Plugins.md b/docs/Plugins.md new file mode 100644 index 000000000..897adf6a8 --- /dev/null +++ b/docs/Plugins.md @@ -0,0 +1,207 @@ +## Enabling plugins + +Gekko currently has a couple plugins: + +- trading advisor (run a TA strategy against a market) +- trader (execute advice from the TA strategy on a real exchange) +- advice logger +- profit simulator +- Mailer +- IRC bot +- Campfire bot +- Redis beacon +- XMP Bot + +To configure a plugin, open up your `config.js` file with a text editor and configure the appropiate section. + +## Trading Advisor + +If you want Gekko to provide automated trading advice you need to configure this in Gekko. Note that this is unrelated to automatic trading which is a plugin that creates order based on this advice. (So if you want automated trading you need both this advice as well as the auto trader.) + +Documentation about trading methods in Gekko can be found [here](./Trading_methods.md). + +### Trader + +This plugin automatically creates orders based on the advice on the market it is watching. This turns Gekko into an automated trading bot. + +Before Gekko can automatically trade you need to create API keys so that Gekko has the rights to create orders on your behalf, the rights Gekko needs are (naming differs per exchange): get info, get balance/portfolio, get open orders, get fee, buy, sell and cancel order. For all exchanges you need the API key and the API secret, for both Bitstamp and CEX.io you also need your username (which is a number at Bitstamp). + +Configure it like this: + + config.trader = { + enabled: true, + key: 'your-api-key', + secret: 'your-api-secret', + username: 'your-username' // your username, only fill in when using bitstamp or cexio + } + +- enabled indicates whether this is on or off. +- key is your API key. +- secret is your API secret. +- username is the username (only required for CEX.io and Bitstamp). + +### Advice logger + +The advice logger is a small plugin that logs new advice calculated by Gekko as soon as there is any. Go to the config and configure it like this: + + config.adviceLogger = { + enabled: true + } + +- enabled indicates whether this is on or off. + +The advice logged advice will look something like this in the terminal: + + 2014-01-15 14:31:44 (INFO): We have new trading advice! + 2014-01-15 14:31:44 (INFO): Position to take: long + 2014-01-15 14:31:44 (INFO): Market price: 5.96 + 2014-01-15 14:31:44 (INFO): Based on market time: 2014-01-15 14:31:01 + +### profit simulator (paper trader) + +The calculator listens to Gekko's advice and on a sell it will swap all (simulated) currency into (simulated) assets at the current price. On a buy it will be the other way around. + +Go to the config and configure it like this: + + // do you want Gekko to calculate the profit of its own advice? + config.profitSimulator = { + enabled: true, + // report the profit in the currency or the asset? + reportInCurrency: true, + // start balance, on what the current balance is compared with + simulationBalance: { + // these are in the unit types configured in the watcher. + asset: 1, + currency: 100, + }, + // only want report after a sell? set to `false`. + verbose: false, + // how much fee in % does each trade cost? + fee: 0.6, + // how much slippage should Gekko assume per trade? + slippage: 0.1 + } + +- enabled indicates whether this is on or off. +- reportInCurrency tells Gekko whether it should report in asset or in the currency. +- simulationBalance tells Gekko with what balance it should start. +- verbose specifies how often Gekko should log the results (false is after every trade, true is after every candle). +- fee is the exchange fee (in %) Gekko should take into considarion when simulating orders. +- slippage is the costs in (in %) associated with not being able to buy / sell at market price.* + +*If you are trading a lot and you are buying 100% currency you might not get it all at market price and you have to walk the book in order to take that position. Also note that Gekko uses the candle close price and is unaware of the top asks bids, also take this into account. It is important that you set this number correctly or the resulted calculated profit be very wrong. Read more information [here](http://www.investopedia.com/terms/s/slippage.asp). Take these into consideration when setting a slippage: + +- How much spread is there normally on this market? +- How thick is the top of the book normally? +- How volatile is this market (the more volatility the bigger the change you will not get the price you expected)? + +The output will be something like: + + 2013-06-02 18:21:15 (INFO): ADVICE is to SHORT @ 117.465 (0.132) + 2013-06-02 18:21:15 (INFO): (PROFIT REPORT) original balance: 207.465 USD + 2013-06-02 18:21:15 (INFO): (PROFIT REPORT) current balance: 217.465 USD + 2013-06-02 18:21:15 (INFO): (PROFIT REPORT) profit: 10.000 USD (4.820%) + +### Mailer + +Mailer will automatically email you whenever Gekko has a new advice. + + // want Gekko to send a mail on buy or sell advice? + config.mailer = { + enabled: false, // Send Emails if true, false to turn off + sendMailOnStart: true, // Send 'Gekko starting' message if true, not if false + + Email: 'me@gmail.com', // Your GMail address + + // You don't have to set your password here, if you leave it blank we will ask it + // when Gekko's starts. + // + // NOTE: Gekko is an open source project < https://github.com/askmike/gekko >, + // make sure you looked at the code or trust the maintainer of this bot when you + // fill in your email and password. + // + // WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT + // guarantuee that your email address & password are safe! + + password: '', // Your GMail Password - if not supplied Gekko will prompt on startup. + tag: '[GEKKO] ', // Prefix all EMail subject lines with this + } + +- enabled indicates whether this is on or off. +- sendMailOnStart will email you right away after Gekko started, you can also use this if you are automatically restarting Gekko to see crash behaviour. +- Email is your email address from which Gekko will send emails (to the same address). +- password is the email password: Gekko needs to login to your account to send emails to you: + + > You don't have to set your password here, if you leave it blank we will ask it + > when Gekko's starts. + > + > NOTE: Gekko is an open source project < https://github.com/askmike/gekko >, + > make sure you looked at the code or trust the maintainer of this bot when you + > fill in your email and password. + > + > WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT + > guarantuee that your email address & password are safe! + +- tag is some text that Gekko will put in all subject lines so you can easily group all advices together. + +### IRC bot + +IRC bot is a small plugin that connects Gekko to an IRC channel and lets users interact with it using basic commands. + + config.ircbot = { + enabled: false, + emitUpdats: false, + channel: '#your-channel', + server: 'irc.freenode.net', + botName: 'gekkobot' + } + +- enabled indicates whether this is on or off. +- emitUpdates tells Gekko that whenever there is a new advice it should broadcast this in the channel. +- channel is the IRC channel the bot will connect to. +- server is the IRC server. +- botName is the username Gekko will use. + +### Campfire bot + +Campfire bot is a small plugin that connects Gekko to a Campfire room and lets users interact with it using basic commands. + + config.campfire = { + enabled: false, + emitUpdats: false, + nickname: 'Gordon', + roomId: 673783, + apiKey: 'e3b0c44298fc1c149afbf4c8996', + account: 'your-subdomain' + } + +- enabled indicates whether this is on or off. +- emitUpdates tells Gekko that whenever there is a new advice it should broadcast this in the room. +- roomId is the ID of the Campfire room the bot will connect to. +- apiKey is the API key for the Campfire user Gekko will connect using. +- account is the subdomain for the account that the room belongs to. + +### Redis beacon + +This is an advanced plugin only for programmers! If you are interested in this read more [here](https://github.com/askmike/gekko/blob/stable/docs/internals/plugins.md#redis-beacon). + + config.redisBeacon = { + enabled: false, + port: 6379, // redis default + host: '127.0.0.1', // localhost + // On default Gekko broadcasts + // events in the channel with + // the name of the event, set + // an optional prefix to the + // channel name. + channelPrefix: '', + broadcast: [ + 'small candle' + ] + } + +- enabled indicates whether this is on or off. +- port is the port redis is running on. +- host is the redis host. +- channelPrefix a string that Gekko will prefix all candles with. +- broadcast is a list of all events you want Gekko to publish. diff --git a/docs/Trading_methods.md b/docs/Trading_methods.md new file mode 100644 index 000000000..6f85a0211 --- /dev/null +++ b/docs/Trading_methods.md @@ -0,0 +1,165 @@ +# Trading Methods + +Gekko implements [technical analysis strategies](http://www.investopedia.com/articles/active-trading/102914/technical-analysis-strategies-beginners.asp) using trading methods. These methods use a number of *[indicators](http://www.investopedia.com/terms/t/technicalindicator.asp)* to calculate an *investment advice*. + +This investment advice is going to be either **long** or **short**. + +Below you can find simple and limited trading methods that come with Gekko, if you are feeling adventurous you can [write your own](internals/trading_methods.md). + +## NOTE + +On default Gekko does nothing with this advice, Gekko uses [plugins](./Plugins.md) that can do something with this advice: + + - trader: trade live on the markets (Gekko becomes a trading bot) + - profit simulator: simulate trading on advice (Gekko becomes a paper trader) + - mailer: automatically email advice (Gekko helps in systematic trading) + - etc.. + +## Enabling a trading method in Gekko + +Open up the config.js file again and configure at this part: + + config.tradingAdvisor = { + enabled: true, + method: 'DEMA', + candleSize: 5, + historySize: 20, + talib: { + enabled: false, + version: '1.0.2' + } + } + +- `enabled` tells gekko it should calculate advice. +- `method` tells gekko what indicator it should calculate (see below for supported methods). +- `candleSize` tells Gekko the size of the candles (in minutes) you want to calculate the trading method over. If you want MACD advice over hourly candles set this to 60. +- `historySize` tells gekko how much historical candles Gekko needs before it can calculate the initial advice. +- `talib` tells gekko whether [talib](https://www.npmjs.com/package/talib) indicators are needed for your method (`false` unless you know what are doing). + +Gekko currently supports: + + - [DEMA](#DEMA) + - [MACD](#MACD) + - [PPO](#PPO) + - [RSI](#RSI) + - [StochRSI](#StochRSI) + - [CCI](#CCI) + - [talib-macd](#talib-macd) + +But you can easily create your custom method, read [here](./internals/trading_methods.md) how! + +### DEMA + +This method uses `Exponential Moving Average crossovers` to determine the current trend the +market is in. Using this information it will suggest to ride the trend. Note that this is +not MACD because it just checks whether the longEMA and shortEMA are [threshold]% removed +from eachother. + +This method is fairly popular in bitcoin trading due to Bitcointalk user Goomboo. Read more about this method in [his topic](https://bitcointalk.org/index.php?topic=60501.0) + +You can configure these parameters for DEMA in config.js: + + config.DEMA = { + // EMA weight (α) + // the higher the weight, the more smooth (and delayed) the line + short: 10, + long: 21, + // amount of candles to remember and base initial EMAs on + // the difference between the EMAs (to act as triggers) + thresholds: { + down: -0.025, + up: 0.025 + } + }; + +- short is the short EMA that moves closer to the real market (including noise) +- long is the long EMA that lags behind the market more but is also more resistant to noise. +- the down treshold and the up treshold tell Gekko how big the difference in the lines needs to be for it to be considered a trend. If you set these to 0 each line cross would trigger new advice. + +### MACD + +This method is similar to DEMA but goes a little further by comparing the difference by an EMA of itself. Read more about it [here](http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_average_conve). + +You can configure these parameters for MACD in config.js: + + // MACD settings: + config.MACD = { + // EMA weight (α) + // the higher the weight, the more smooth (and delayed) the line + short: 10, + long: 21, + signal: 9, + // the difference between the EMAs (to act as triggers) + thresholds: { + down: -0.025, + up: 0.025, + // How many candle intervals should a trend persist + // before we consider it real? + persistence: 1 + } + }; + +- short is the short EMA that moves closer to the real market (including noise) +- long is the long EMA that lags behind the market more but is also more resistant to noise. +- signal is the EMA weight calculated over the difference from short/long. +- the down treshold and the up treshold tell Gekko how big the difference in the lines needs to be for it to be considered a trend. If you set these to 0 each line cross would trigger new advice. +- persistence tells Gekko how long the thresholds needs to be met until Gekko considers the trend to be valid. + +### PPO + +Very similar to MACD but also a little different, read more [here](http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:price_oscillators_pp). + + // PPO settings: + config.PPO = { + // EMA weight (α) + // the higher the weight, the more smooth (and delayed) the line + short: 12, + long: 26, + signal: 9, + // the difference between the EMAs (to act as triggers) + thresholds: { + down: -0.025, + up: 0.025, + // How many candle intervals should a trend persist + // before we consider it real? + persistence: 2 + } + }; + +- short is the short EMA that moves closer to the real market (including noise) +- long is the long EMA that lags behind the market more but is also more resistant to noise. +- signal is the EMA weight calculated over the difference from short/long. +- the down treshold and the up treshold tell Gekko how big the difference in the lines needs to be for it to be considered a trend. If you set these to 0 each line cross would trigger new advice. +- persistence tells Gekko how long the thresholds needs to be met until Gekko considers the trend to be valid. + +### RSI + +The Relative Strength Index is a momentum oscillator that measures the speed and change of price movements. Read more about it [here](http://stockcharts.com/help/doku.php?id=chart_school:technical_indicators:relative_strength_in). Configure it like so: + + // RSI settings: + config.RSI = { + interval: 14, + thresholds: { + low: 30, + high: 70, + // How many candle intervals should a trend persist + // before we consider it real? + persistence: 1 + } + }; + +- The interval is the amount of periods the RSI should use. +- The thresholds determine what level of RSI would trigger an up or downtrend. +- persistence tells Gekko how long the thresholds needs to be met until Gekko considers the trend to be valid. + +### StochRSI + +TODO! + +### CCI + +TODO! + +### talib-macd + +TODO! \ No newline at end of file diff --git a/docs/installing_gekko_on_windows.md b/docs/installing_gekko_on_windows.md index c6a32ddec..2f361290c 100644 --- a/docs/installing_gekko_on_windows.md +++ b/docs/installing_gekko_on_windows.md @@ -13,7 +13,7 @@ Gekko runs on nodejs so we have to install that first. Head over the [nodejs hom ## Install Gekko -The easiest way to download Gekko is to go to the [Github repo](https://github.com/askmike/gekko) and click on the 'zip' button at the top. Once you have downloaded the zip file it's easiest to extract it to your Desktop. When you have done that we can begin with the cool stuff: +The easiest way to download Gekko is to go to the [Github repo](https://github.com/askmike/gekko) and click on the 'zip' button at the top. Once you have downloaded the zip file it's the easiest to extract it to your Desktop. When you have done that we can begin with the cool stuff: ### Open up command line @@ -37,7 +37,7 @@ Pre-windows 7: First navigate to Gekko: cd Desktop - cd gekko-master + cd gekko-0.2 Install Gekko's dependencies: @@ -55,6 +55,6 @@ In the command line hold `ctrl` + `c`. ## Configure Gekko -Use Windows Explorer to navigate to the `gekko-master` folder on your desktpop. Open `gekko.js` with a texteditor (like notepad) and check at [the repo](https://github.com/askmike/gekko#install) what settings you can change. +Use Windows Explorer to navigate to the `gekko-stable` folder on your desktpop. Cope the file `sample-config.js` to `config.js`. Open `config.js` with a texteditor (like notepad) and check in [the documentation](https://github.com/askmike/gekko/tree/stable/docs/Configuring_gekko.md) hwo you should configure Gekko. -After changing the settings you have to stop and start Gekko. \ No newline at end of file +Remember, after changing the settings you have to stop and start Gekko! \ No newline at end of file diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index 178a11851..25edabdbd 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -1,11 +1,44 @@ # Gekko's architecture -Gekko is build around an event based architecture. The core consists of different modules that pass data between each other. All internal events are exposed to plugins as well, some parts of core are even implemented as plugins (like the [trading advisor](https://github.com/askmike/gekko/blob/master/plugins/tradingAdvisor.js) and [the trader](https://github.com/askmike/gekko/blob/master/plugins/trader.js)). +![Gekko architecture](https://wizb.it/gekko/static/architecture.jpg) -This image below gives a represenation of how information is propogated inside Gekko. +Every Gekko instance has two core components: -![Gekko 0.1.0 architecture](http://data.wizb.it/misc/gekko-0.1.0-architecture.jpg) +- A market +- A GekkoStream --- +Communicaton between those two components is handled by Node.JS' [Stream API](https://nodejs.org/api/stream.html). The Market implements a Readable Stream interface while the GekkoStream implements a Writeable Stream interface. -Rest is coming soon! \ No newline at end of file +## A Market + +All markets in Gekko eventually output `candle` data. Where these candles come from and how old they are does not matter to the GekkoStream they get piped into. On default Gekko looks for markets in the [`core/markets/` directory](https://github.com/askmike/gekko/tree/stable/core/markets) (but changing that is [not too hard](https://github.com/askmike/gekko/blob/72a858339afb5a856179c716ec4ea13070a6c87c/gekko.js#L48-L49)). The top orange block in the picture is a BudFox market (the default). + +Example Markets that come included with Gekko are: + +- **BudFox** (a realtime market): this market stays on indefinitely and outputs candles from the live markets in semi-realtime (cryptocurrency exchanges run 24/7). +- **backtest**: this market reads candles from a database and outputs them as fast as the GekkoStream is able to consume them (the GekkoStream will run TA strategies over these candles). +- **importer**: the importer market will fetch historical data from an exchange and pass on (to the GekkoStream who inserts them in a database). + +## A GekkoStream + +A GekkoStream is nothing more than a collection of [plugins](https://github.com/askmike/gekko/blob/stable/docs/Plugins.md). Plugins are simple modules that can subscribe to events, and do something based on event data. The most basic event every GekkoStream has is the "candle" event, this event comes from the market. + +However **plugins are allowed to broadcast their own events, which other plugins can subscribe to**. An example of this is the `tradingAdvisor` plugin. This plugin will implement a [trading method](https://github.com/askmike/gekko/blob/stable/docs/Trading_methods.md) that will be fed candle data. As soon as the trading method suggests to take a certain position in the market ("I detect an uptrend, I advice to go **long**") it will broadcast an `advice` event. The `profitSimulator` is a plugin that simulates trading using these advices, the `trader` is a plugin that creates real market orders based on these advices. You can decide to only turn the `profitSimulator` on (you now have a paper trading bot) or to just turn the `trader` on (you now have a trading bot). + +When you run a backtest using Gekko the following things happen: + +- Gekko configures a `backtest` market. +- Gekko loads all configured plugins that are supported in the `backtest` mode* into a GekkoStream. +- Gekko pipes the market into the GekkoStream and voila! + +\*Gekko refuses to load plugins that are unsupported in specific modes. During backtests you **never** want to enable the real trader to enter market orders. Because if you would the advice would be based on specific moments in the backtest, not on the current state of the market. + +## Plugins & Adapters + +Those two core components describe the majority of Gekko's flow. A lot "core functionality" like saving candles to disk are simply plugins that push all candles to a database adapter. + +## Seperated architecture + +The modular nature of Gekko makes it very dynamic and allows for rapidly creating new plugins. However there is an ugly side to this story: + +The `tradingAdvisor` runs TA strategies against a market. The problem however is that most TA indicators need some history before thay can give accurate results. If you want to use an EMA (exponential moving average), you need some history to base the initial average on. But because the tradingAdvisor doesn't know what market data is going to be made available later by the market, it needs to do some fetching itself and compare that to locally available market data (stored in the local database) to see if it can stitch the two sources. \ No newline at end of file diff --git a/docs/internals/budfox.md b/docs/internals/budfox.md new file mode 100644 index 000000000..544ab5037 --- /dev/null +++ b/docs/internals/budfox.md @@ -0,0 +1,41 @@ +# BudFox + +**Similar to the [movie Wallstreet](https://en.wikipedia.org/wiki/Wall_Street_(1987_film)), Gekko delegates the dirty work of getting fresh data to Bud Fox. Bud Fox delivers the data to Gekko who uses this data to make investment decisions.** + +Whenever Gekko works with realtime market data, it spawns a BudFox to fetch and transform the data for every market (exchange + asset + pair, for example: `bitstamp USD/BTC`). Bud Fox will keep on fetching data from the market in semi-realtime, turn historical trades into minutley candles (and make sure every minute of data has a candle). + +BudFox exposes a stream of `candles` which are fed to Gekko. + +## Advanced Usage + +BudFox is a small part of Gekko's core that aggregates realtime market data from any supported exchange into a readable stream of candles. Example usage: + + var config = { + exchange: 'Bitstamp', + currency: 'USD', + asset: 'BTC' + } + + new BudFox(config) + .start() + // convert JS objects to JSON string + .pipe(new require('stringify-stream')()) + // output to standard out + .pipe(process.stdout); + +Outputs: + + {"start":"2015-02-02T23:08:00.000Z","open":238.21,"high":239.35,"low":238.21,"close":238.66,"vwp":8743.778447997309,"volume":203.6969347,"trades":56} + {"start":"2015-02-02T23:09:00.000Z","open":239.03,"high":240,"low":238.21,"close":239.19,"vwp":8725.27119145289,"volume":323.66383462999994,"trades":72} + {"start":"2015-02-02T23:10:00.000Z","open":239.19,"high":239.8,"low":234.68,"close":235,"vwp":6664.509955946812,"volume":114.67727173,"trades":48} + {"start":"2015-02-02T23:11:00.000Z","open":237.77,"high":238.51,"low":235,"close":238.1,"vwp":3158.835462414369,"volume":41.47081054999999,"trades":28} + {"start":"2015-02-02T23:12:00.000Z","open":237,"high":238,"low":236.78,"close":237.9,"vwp":1634.5173557116634,"volume":70.58755061,"trades":22} + {"start":"2015-02-02T23:13:00.000Z","open":237.95,"high":238.49,"low":237.95,"close":238.49,"vwp":604.219141331534,"volume":12.196531389999999,"trades":7} + {"start":"2015-02-02T23:14:00.000Z","open":238.51,"high":241,"low":237.89,"close":241,"vwp":7610.305142999085,"volume":579.5321983399998,"trades":67} + {"start":"2015-02-02T23:15:00.000Z","open":238.12,"high":239.76,"low":238.12,"close":239.1,"vwp":1828.5872875471068,"volume":31.16232463,"trades":17} + {"start":"2015-02-02T23:16:00.000Z","open":239.1,"high":239.76,"low":239.1,"close":239.67,"vwp":1339.3753800771717,"volume":5.56431998,"trades":12} + {"start":"2015-02-02T23:17:00.000Z","open":239.27,"high":239.99,"low":239.25,"close":239.92,"vwp":1519.3392752690336,"volume":6.984999999999999,"trades":14} + {"start":"2015-02-02T23:18:00.000Z","open":239.92,"high":239.98,"low":238.98,"close":238.98,"vwp":4162.807256131301,"volume":21.17212333,"trades":29} + {"start":"2015-02-02T23:19:00.000Z","open":239,"high":239,"low":238.15,"close":238.33,"vwp":1627.2581467076204,"volume":31.682705360000003,"trades":15} + {"start":"2015-02-02T23:20:00.000Z","open":238.33,"high":239.95,"low":238.33,"close":239,"vwp":3648.661808492067,"volume":128.35564560999998,"trades":23} + // etc.. \ No newline at end of file diff --git a/docs/internals/plugins.md b/docs/internals/plugins.md index f65fac967..9c5237878 100644 --- a/docs/internals/plugins.md +++ b/docs/internals/plugins.md @@ -8,7 +8,8 @@ All plugins live in `gekko/plugins`. ## Existing plugins: -- Advice logger: log trading advice in stdout. +- Candle Store: save trades to disk. +- Advice logger: log trading advice in your terminal (stdout). - Mailer: mail trading advice to your gmail account. - IRC bot: logs Gekko on in an irc channel and lets users communicate with it. - Profit simulator: simulates trades and calculates profit over these (and logs profit). @@ -17,55 +18,33 @@ All plugins live in `gekko/plugins`. ## What kind of events can I listen to? -- `trade`: Everytime Gekko refetched an exchange trade data and it has new trades, it will - propogate the most recent one. -- `candle`: Everytime Gekko calculated a new candle (as defined by the candleSize), - it is propogated here. -- `small candle`: Everytime Gekko calculated a new small candle (always 1 minute size - candles), it is propogated here. -- `advice`: Everytime an implemented trading method suggests you change your position. +- `candle`: Everytime Gekko calculated a new 1 minute candle from the market. +- `advice`: Everytime the trading strategy has new advice. ## Implementing a new plugin +**TODO: update** + If you want to add your own plugin you need to expose a constructor function inside `plugins/[slugname of plugin].js`. The object needs methods based on which event you want to listen to: -- market feed / trade: `processTrade`. - This method will be fed a trade like: - - { - date: [unix timestamp], - price: [price of asset], - tid: [trade id], - amount: [volume of trade] - } - - market feed / candle: `processCandle`. - This method will be fed a candle like: + This method will be fed a 1 minute candle like: { - start: [moment object], - o: [open of candle], - h: [high of candle], - l: [low of candle], - c: [close of candle], - p: [average weighted price of candle], - v: [volume] + start: [moment object of the start time of the candle], + open: [open of candle], + high: [high of candle], + low: [low of candle], + close: [close of candle], + vwp: [average weighted price of candle], + volume: [total volume volume], + trades: [amount of trades] } -- market feed / small candle: `processSmallCandle`. - This method will be fed a candle like: - - { - start: [moment object], - o: [open of candle], - h: [high of candle], - l: [low of candle], - c: [close of candle], - p: [average weighted price of candle], - v: [volume] - } + As well as a callback method. You are required to call this method + once you are done processing the candle. - advice feed / advice `processAdvice`: This method will be fed an advice like: @@ -76,8 +55,7 @@ to listen to: } You also need to add an entry for your plugin inside `plugins.js` which tells Gekko a little about -your plugin. Finally you need to add a close -to `config.js` with atleast: +your plugin. Finally you need to add a configuration object to `config.js` with atleast: config.[slug name of plugin] = { enabled: true @@ -85,15 +63,4 @@ to `config.js` with atleast: Besides enabled you can also add other configurables here which users can set themselves. -That's it, don't forget to create a pull request of the awesome plugin you've just created! - -# Redis Beacon - -Gekko also has an plugin which can pipe all events through [redis pubsub](http://redis.io/topics/pubsub), this means that you can also build something on top of Gekko's events with the freedom to: - -- Write the plugins in your language of choice (as long as you can connect it to redis). -- Run the plugin outside of the a sandbox / contained environment within Gekko. -- Run plugin on a different machine (scale it, etc) -- Write one which can listen to a cluster Gekkos at the same time. -- *(Theoretical) Create a cluster of Gekkos where a single one fetches market data and all the ones on top run different trading methods.* - +That's it! Don't forget to create a pull request of the awesome plugin you've just created! \ No newline at end of file diff --git a/docs/internals/trading_methods.md b/docs/internals/trading_methods.md index 6de541d78..6eaea15f0 100644 --- a/docs/internals/trading_methods.md +++ b/docs/internals/trading_methods.md @@ -1,8 +1,8 @@ # Trading Methods -The trading methods are the core of Gekko's trading bot. They look at the market and decide what to do based on technical analysis. The trading methods can calculate indicators on top of the market data to come up with an advice for a position to take in the market. As of now the trading method is limited to a single market on a single exchange. +The trading methods are the core of Gekko's trading bot. They look at the market and decide what to do based on technical analysis indicators. As of now the trading method is limited to a single market on a single exchange. -Gekko currently has a couple of methods already, only implementing a single indicator: DEMA, MACD, RSI and PPO. Besides those you can also create your own trading method by using javascript. The easiest way to do this is open the file `gekko/methods/custom.js` and write your own trading method. +Gekko currently comes with [a couple of methods](../Trading_methods.md) out of box. Besides those you can also create your own trading method by using javascript. The easiest way to do this is open the file `gekko/methods/custom.js` and write your own trading method. ## Creating a trading method @@ -69,10 +69,10 @@ If you find out in the check function that you want to give a new advice to the ## Trading method rules -- You can activate your own method by setting `config.tradingAdvisor.method` to `custom` in the loaded config. -- Gekko will execute the functions for every candle. A candle is the size configured at `config.tradingAdvisor.candleSize` in the loaded config. +- You can activate your own method by setting `config.tradingAdvisor.method` to `custom` (or whatever you named your script inside the `gekko/methods`) in the loaded config. +- Gekko will execute the functions for every candle. A candle is the size in minutes configured at `config.tradingAdvisor.candleSize` in the loaded config. - It is advised to set history `config.tradingAdvisor.historySize` the same as the requiredHistory as Gekko will use this property to create an initial batch of candles. -- Never rely on anything based on system or candle time because each method can run on live markets as well as during backtesting. +- Never rely on anything based on system time because each method can run on live markets as well as during backtesting. ## Trading method tools @@ -84,6 +84,10 @@ If you want to use an indicator you can add it in the `init` function and Gekko this.addIndicator('name', 'type', parameters); +or + + this.addTalibIndicator('name', 'type', parameters); + The first parameter is the name, the second is the indicator type you want and the third is an object with all indicator parameters. If you want an MACD indicator you can do it like so: In your init method: @@ -95,9 +99,7 @@ In your check or update method: var result = this.indicators.mymacd.result; -#### Supported indicators - -Gekko currently supports EMA, SMA, PPO, RSI and MACD. +Currently supported native indicators can be found [here](https://github.com/askmike/gekko/tree/stable/methods/indicators), all talib indicators (100+) can be found [here](http://ta-lib.org/function.html). ### Configurables @@ -140,4 +142,4 @@ Gekko has a small logger you can use (preferably in your log method): ----- -Take a look at the existing methods, if you have questions feel free to create an issue. \ No newline at end of file +Take a look at the existing methods, if you have questions feel free to create an issue. If you created your own awesome trading method and want to share it with the world feel free to contribute it to gekko. \ No newline at end of file diff --git a/docs/supported_exchanges.md b/docs/supported_exchanges.md new file mode 100644 index 000000000..41a7065e1 --- /dev/null +++ b/docs/supported_exchanges.md @@ -0,0 +1,3 @@ +# Supported Exchanges + +TODO. \ No newline at end of file diff --git a/exchanges/bitstamp.js b/exchanges/bitstamp.js index b73125bfd..7170c6204 100644 --- a/exchanges/bitstamp.js +++ b/exchanges/bitstamp.js @@ -10,6 +10,7 @@ var Trader = function(config) { this.key = config.key; this.secret = config.secret; this.clientID = config.username; + this.market = (config.asset + config.currency).toLowerCase(); } this.name = 'Bitstamp'; this.balance; @@ -43,6 +44,10 @@ Trader.prototype.retry = function(method, args) { Trader.prototype.getPortfolio = function(callback) { var set = function(err, data) { + + if(!_.isEmpty(data.error)) + return callback('BITSTAMP API ERROR: ' + data.error); + var portfolio = []; _.each(data, function(amount, asset) { if(asset.indexOf('available') !== -1) { @@ -51,12 +56,13 @@ Trader.prototype.getPortfolio = function(callback) { } }); callback(err, portfolio); - } - this.bitstamp.balance(_.bind(set, this)); + }.bind(this); + + this.bitstamp.balance(this.market, set); } Trader.prototype.getTicker = function(callback) { - this.bitstamp.ticker(callback); + this.bitstamp.ticker(this.market, callback); } Trader.prototype.getFee = function(callback) { @@ -65,8 +71,9 @@ Trader.prototype.getFee = function(callback) { callback(err); callback(false, data.fee / 100); - } - this.bitstamp.balance(_.bind(set, this)); + }.bind(this); + + this.bitstamp.balance(this.market, set); } Trader.prototype.buy = function(amount, price, callback) { @@ -75,7 +82,7 @@ Trader.prototype.buy = function(amount, price, callback) { return log.error('unable to buy:', err, result); callback(null, result.id); - }; + }.bind(this); // TODO: fees are hardcoded here? amount *= 0.995; // remove fees @@ -83,7 +90,14 @@ Trader.prototype.buy = function(amount, price, callback) { amount *= 100000000; amount = Math.floor(amount); amount /= 100000000; - this.bitstamp.buy(amount, price, _.bind(set, this)); + + // prevent: + // 'Ensure that there are no more than 2 decimal places.' + price *= 100; + price = Math.floor(price); + price /= 100; + + this.bitstamp.buy(this.market, amount, price, undefined, set); } Trader.prototype.sell = function(amount, price, callback) { @@ -92,27 +106,33 @@ Trader.prototype.sell = function(amount, price, callback) { return log.error('unable to sell:', err, result); callback(null, result.id); - }; + }.bind(this); - this.bitstamp.sell(amount, price, _.bind(set, this)); + // prevent: + // 'Ensure that there are no more than 2 decimal places.' + price *= 100; + price = Math.ceil(price); + price /= 100; + + this.bitstamp.sell(this.market, amount, price, undefined, set); } Trader.prototype.checkOrder = function(order, callback) { var check = function(err, result) { var stillThere = _.find(result, function(o) { return o.id === order }); callback(err, !stillThere); - }; + }.bind(this); - this.bitstamp.open_orders(_.bind(check, this)); + this.bitstamp.open_orders(this.market, check); } Trader.prototype.cancelOrder = function(order, callback) { var cancel = function(err, result) { if(err || !result) log.error('unable to cancel order', order, '(', err, result, ')'); - }; + }.bind(this); - this.bitstamp.cancel_order(order, _.bind(cancel, this)); + this.bitstamp.cancel_order(order, cancel); } Trader.prototype.getTrades = function(since, callback, descending) { @@ -122,10 +142,12 @@ Trader.prototype.getTrades = function(since, callback, descending) { return this.retry(this.getTrades, args); callback(null, result.reverse()); - }; + }.bind(this); - this.bitstamp.transactions(_.bind(process, this)); + if(since) + this.bitstamp.transactions(this.market, {time: since}, process); + else + this.bitstamp.transactions(this.market, process); } - module.exports = Trader; \ No newline at end of file diff --git a/exchanges/bitx.js b/exchanges/bitx.js new file mode 100644 index 000000000..30cecbb90 --- /dev/null +++ b/exchanges/bitx.js @@ -0,0 +1,96 @@ +var BitX = require("bitx") +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + } + this.name = 'BitX'; + this.pair = config.asset + config.currency; + this.bitx = new BitX(this.key, this.secret, { pair: this.pair }); + +} + +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) }, + wait + ); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) + return this.retry(this.getTrades, args); + + trades = _.map(result.trades, function (t) { + return { + price: t.price, + date: Math.round(t.timestamp / 1000), + amount: t.volume, + msdate: t.timestamp // we use this as tid + }; + }); + + // Decending by default + if (!descending) { + trades = trades.reverse() + } + + callback(null, trades); + }.bind(this); + + this.bitx.getTrades(process); +} + + +Trader.prototype.buy = function(amount, price, callback) { + +} + +Trader.prototype.sell = function(amount, price, callback) { + +} + +Trader.prototype.getPortfolio = function(callback) { + +} + +Trader.prototype.getTicker = function(callback) { + +} + +Trader.prototype.getFee = function(callback) { + +} + +Trader.prototype.checkOrder = function(order, callback) { + +} + +Trader.prototype.cancelOrder = function(order) { + +} + + +module.exports = Trader; diff --git a/exchanges/btcc.js b/exchanges/btcc.js new file mode 100644 index 000000000..2a2bb5326 --- /dev/null +++ b/exchanges/btcc.js @@ -0,0 +1,156 @@ +var BTCChina = require('btc-china-fork'); +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + this.clientID = config.username; + } + this.name = 'BTCC'; + + this.pair = (config.asset + config.currency).toUpperCase(); + + this.btcc = new BTCChina(this.key, this.secret, this.clientID); +} + +// if the exchange errors we try the same call again after +// waiting 10 seconds +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) }, + wait + ); +} + +Trader.prototype.getTicker = function(callback) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) + return this.retry(this.getTicker, args); + + callback(null, { + bid: result.bids[0][0], + ask: result.asks[0][0] + }); + + }.bind(this); + + this.btcc.getOrderBook(process, this.pair, 1); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) + return this.retry(this.getTrades, args); + + if(descending) + callback(null, result.reverse()); + else + callback(null, result); + }.bind(this); + + if(!since) + since = 500; + + this.btcc.getHistoryData(process, {limit: since}); +} + +Trader.prototype.getPortfolio = function(callback) { + var args = _.toArray(arguments); + var set = function(err, data) { + if(err) + return this.retry(this.getPortfolio, args); + + var portfolio = []; + _.each(data.result.balance, function(obj) { + portfolio.push({name: obj.currency, amount: parseFloat(obj.amount)}); + }); + callback(err, portfolio); + }.bind(this); + + this.btcc.getAccountInfo(set, 'ALL'); +} + +Trader.prototype.getFee = function(callback) { + var args = _.toArray(arguments); + var set = function(err, data) { + if(err) + this.retry(this.getFee, args); + + callback(false, data.result.profile.trade_fee / 100); + }.bind(this); + + this.btcc.getAccountInfo(set, 'ALL'); +} + +Trader.prototype.buy = function(amount, price, callback) { + // TODO: do somewhere better.. + amount = Math.floor(amount * 10000) / 10000; + + var set = function(err, result) { + if(err) + return log.error('unable to buy:', err, result); + + callback(null, result.result); + }.bind(this); + + this.btcc.createOrder2(set, 'buy', price, amount, this.pair); +} + +Trader.prototype.sell = function(amount, price, callback) { + // TODO: do somewhere better.. + amount = Math.round(amount * 10000) / 10000; + + var set = function(err, result) { + if(err) + return log.error('unable to sell:', err, result); + + callback(null, result.result); + }.bind(this); + + this.btcc.createOrder2(set, 'sell', price, amount, this.pair); +} + +Trader.prototype.checkOrder = function(order, callback) { + var args = _.toArray(arguments); + var check = function(err, result) { + if(err) + this.retry(this.checkOrder, args); + + var done = result.result.order.status === 'closed'; + callback(err, done); + }; + + this.btcc.getOrder(check, order, this.pair, true); +} + +Trader.prototype.cancelOrder = function(order, callback) { + var cancel = function(err, result) { + if(err) + log.error('unable to cancel order', order, '(', err, result, ')'); + }.bind(this); + + this.btcc.cancelOrder(cancel, order, this.pair); +} + +module.exports = Trader; \ No newline at end of file diff --git a/exchanges/btce.js b/exchanges/btce.js index 0931a0e6f..7e035b070 100644 --- a/exchanges/btce.js +++ b/exchanges/btce.js @@ -14,6 +14,11 @@ var Trader = function(config) { _.bindAll(this); this.btce = new BTCE(this.key, this.secret); + _.bindAll(this.btce, ['trade', 'trades', 'getInfo', 'ticker', 'orderList']); + + // see @link https://github.com/askmike/gekko/issues/302 + this.btceHistorocial = new BTCE(false, false, {public_url: 'https://btc-e.com/api/3/'}); + _.bindAll(this.btceHistorocial, 'trades'); } Trader.prototype.round = function(amount) { @@ -34,12 +39,12 @@ Trader.prototype.buy = function(amount, price, callback) { return log.error('unable to buy:', err); callback(null, data.order_id); - }; + }.bind(this); // workaround for nonce error - setTimeout(_.bind(function() { + setTimeout(function() { this.btce.trade(this.pair, 'buy', price, amount, _.bind(set, this)); - }, this), 1000); + }.bind(this), 1000); } Trader.prototype.sell = function(amount, price, callback) { @@ -53,9 +58,9 @@ Trader.prototype.sell = function(amount, price, callback) { }; // workaround for nonce error - setTimeout(_.bind(function() { + setTimeout(function() { this.btce.trade(this.pair, 'sell', price, amount, _.bind(set, this)); - }, this), 1000); + }.bind(this), 1000); } // if the exchange errors we try the same call again after @@ -66,13 +71,6 @@ Trader.prototype.retry = function(method, args) { var self = this; - // make sure the callback (and any other fn) - // is bound to Trader - _.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( @@ -90,28 +88,33 @@ Trader.prototype.getPortfolio = function(callback) { return this.retry(this.btce.getInfo, calculate); } - var portfolio = []; _.each(data.funds, function(amount, asset) { portfolio.push({name: asset.toUpperCase(), amount: amount}); }); callback(err, portfolio); - } - this.btce.getInfo(_.bind(calculate, this)); + }.bind(this); + + this.btce.getInfo(calculate); } Trader.prototype.getTicker = function(callback) { // BTCE-e doesn't state asks and bids in its // ticker var set = function(err, data) { + + if(err) + return this.retry(this.btce.ticker, [this.pair, set]); + var ticker = _.extend(data.ticker, { ask: data.ticker.buy, bid: data.ticker.sell }); callback(err, ticker); - } - this.btce.ticker(this.pair, _.bind(set, this)); + }.bind(this); + + this.btce.ticker(this.pair, set); } Trader.prototype.getFee = function(callback) { @@ -153,8 +156,31 @@ Trader.prototype.getTrades = function(since, callback, descending) { callback(false, trades); else callback(false, trades.reverse()); - } - this.btce.trades(this.pair, _.bind(process, this)); + }.bind(this); + + // see @link https://github.com/askmike/gekko/issues/302 + if(since) { + this.btceHistorocial.makePublicApiRequest( + 'trades', + this.pair + since, + this.processAPIv3Trades(process) + ) + } else + this.btce.trades(this.pair, process); +} + +Trader.prototype.processAPIv3Trades = function(cb) { + return function(err, data) { + var trades = _.map(data[this.pair], function(t) { + return { + price: t.price, + amount: t.amount, + tid: t.tid, + date: t.timestamp + } + }) + cb(err, trades); + }.bind(this) } module.exports = Trader; diff --git a/exchanges/bx.in.th.js b/exchanges/bx.in.th.js new file mode 100644 index 000000000..1d8306a9d --- /dev/null +++ b/exchanges/bx.in.th.js @@ -0,0 +1,73 @@ +var BitexthaiAPI = require('bitexthai'); +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + this.clientID = config.username; + } + this.name = 'BX.in.th'; + + this.pair = 1; // todo + + this.bitexthai = new BitexthaiAPI(this.key, this.secret, this.clientID); +} + +// if the exchange errors we try the same call again after +// waiting 10 seconds +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) }, + wait + ); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) + return this.retry(this.getTrades, args); + + var parsedTrades = []; + _.each(result.trades, function(trade) { + // We get trade time in local time, which is GMT+7 + var date = moment(trade.trade_date).subtract(7, 'hours').unix(); + + parsedTrades.push({ + date: date, + price: parseFloat(trade.rate), + amount: parseFloat(trade.amount), + tid: trade.trade_id + }); + }, this); + + if(descending) + callback(null, parsedTrades.reverse()); + else + callback(null, parsedTrades); + }.bind(this); + + this.bitexthai.trades(this.pair, process); +} + + +module.exports = Trader; \ No newline at end of file diff --git a/exchanges/cexio.js b/exchanges/cexio.js index 0494597ba..4994b8b37 100644 --- a/exchanges/cexio.js +++ b/exchanges/cexio.js @@ -186,3 +186,4 @@ Trader.prototype.cancelOrder = function(order) { } module.exports = Trader; + diff --git a/exchanges/kraken.js b/exchanges/kraken.js index cdb7e3a70..a37ef84ac 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -6,16 +6,20 @@ var log = require('../core/log'); var crypto_currencies = [ "LTC", - "NMC", "XBT", - "XVN", + "XRP", + "DAO", + "ETH", + "XDG", + "XLM", "XRP" ]; var fiat_currencies = [ "EUR", - "KRW", - "USD" + "GBP", + "USD", + "JPY" ]; // Method to check if asset/currency is a crypto currency @@ -53,12 +57,16 @@ Trader.prototype.setAssetPair = function() { var assetPrefix = "X"; var currencyPrefix = "Z"; - if (isFiat(this.asset)) { + if (isFiat(this.asset)) assetPrefix = "Z"; - } - if (isCrypto(this.currency)) { + else if(isCrypto(this.currency)) + assetPrefix = "X"; + + + if (isFiat(this.currency)) + currencyPrefix = "Z"; + else if(isCrypto(this.currency)) currencyPrefix = "X"; - } this.pair = assetPrefix + this.asset + currencyPrefix + this.currency; }; @@ -67,7 +75,7 @@ Trader.prototype.retry = function(method, args, err) { var wait = +moment.duration(10, 'seconds'); log.debug(this.name, 'returned an error, retrying..', err); - if (!_.isFunction(method)) { + if(!_.isFunction(method)) { log.error(this.name, 'failed to retry, no method supplied.'); return; } @@ -104,9 +112,7 @@ Trader.prototype.getTrades = function(since, callback, descending) { }); }, this); - this.since = trades.result.last; - - if (descending) + if(descending) callback(null, parsedTrades.reverse()); else callback(null, parsedTrades); @@ -115,17 +121,23 @@ Trader.prototype.getTrades = function(since, callback, descending) { var reqData = { pair: this.pair }; - if (!_.isNull(this.since)) - reqData.since = this.since; - + // This appears to not work correctly + // skipping for now so we have the same + // behaviour cross exchange. + // + // if(!_.isNull(this.since)) + // reqData.since = this.since; this.kraken.api('Trades', reqData, _.bind(process, this)); }; Trader.prototype.getPortfolio = function(callback) { var args = _.toArray(arguments); var set = function(err, data) { - if (err || !_.isEmpty(data.error)) - return this.retry(this.getPortfolio, args, JSON.stringify(data.error)); + if(!_.isEmpty(data.error)) + err = data.error; + + if (err) + return this.retry(this.getPortfolio, args, JSON.stringify(err)); var portfolio = []; _.each(data.result, function(amount, asset) { @@ -143,8 +155,11 @@ Trader.prototype.getFee = function(callback) { Trader.prototype.getTicker = function(callback) { var set = function(err, data) { - if (err || !_.isEmpty(data.error)) - return log.error('unable to get ticker', JSON.stringify(data.error)); + if(!_.isEmpty(data.error)) + err = data.error; + + if (err) + return log.error('unable to get ticker', JSON.stringify(err)); var result = data.result[this.pair]; var ticker = { @@ -172,13 +187,16 @@ Trader.prototype.addOrder = function(tradeType, amount, price, callback) { log.debug(tradeType.toUpperCase(), amount, this.asset, '@', price, this.currency); var set = function(err, data) { - if (err || !_.isEmpty(data.error)) - return log.error('unable to', tradeType.toLowerCase(), JSON.stringify(data.error)); + if(!_.isEmpty(data.error)) + err = data.error; + + if (err) + return log.error('unable to', tradeType.toLowerCase(), JSON.stringify(err)); var txid = data.result.txid[0]; log.debug('added order with txid:', txid); - callback(txid); + callback(err, txid); }; this.kraken.api('AddOrder', { @@ -200,8 +218,11 @@ Trader.prototype.sell = function(amount, price, callback) { Trader.prototype.checkOrder = function(order, callback) { var check = function(err, data) { - if (err || !_.isEmpty(data.error)) - return log.error('unable to check order', order, JSON.stringify(data.error)); + if(!_.isEmpty(data.error)) + err = data.error; + + if(err) + return log.error('Unable to check order', order, JSON.stringify(err)); var result = data.result[order]; var stillThere = result.status === 'open' || result.status === 'pending'; @@ -213,8 +234,11 @@ Trader.prototype.checkOrder = function(order, callback) { Trader.prototype.cancelOrder = function(order) { var cancel = function(err, data) { - if (err || !_.isEmpty(data.error)) - log.error('unable to cancel order', order, '(', err, JSON.stringify(data.error), ')'); + if(!_.isEmpty(data.error)) + err = data.error; + + if(err) + log.error('unable to cancel order', order, '(', err, JSON.stringify(err), ')'); }; this.kraken.api('CancelOrder', {txid: order}, _.bind(cancel, this)); diff --git a/exchanges/mexbt.js b/exchanges/mexbt.js new file mode 100644 index 000000000..81aabc3dc --- /dev/null +++ b/exchanges/mexbt.js @@ -0,0 +1,138 @@ +var Mexbt = require("mexbt"); +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + this.userId = config.username; + this.pair = config.asset || this.currency; + } + this.name = 'meXBT'; + this.mexbt = new Mexbt(this.key, this.secret, this.userId); +} + +// if the exchange errors we try the same call again after +// waiting 10 seconds +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) }, + wait + ); +} + +Trader.prototype.getPortfolio = function(callback) { + var set = function(err, data) { + var portfolio = []; + _.each(data.currencies, function(balanceInfo) { + portfolio.push({name: balanceInfo.name, amount: balanceInfo.balance}); + }); + callback(err, portfolio); + } + this.mexbt.accountBalance(_.bind(set, this)); +} + +Trader.prototype.getTicker = function(callback) { + this.mexbt.ticker(callback); +} + +Trader.prototype.getFee = function(callback) { + var set = function(err, data) { + if(err) + callback(err); + + callback(false, data.fee); + } + this.mexbt.getTradingFee({amount: 1, type: 'limit'}, _.bind(set, this)); +} + +Trader.prototype.buy = function(amount, price, callback) { + var set = function(err, result) { + if(err || result.error) + return log.error('unable to buy:', err, result); + + callback(null, result.serverOrderId); + }; + + this.mexbt.createOrder({amount: amount, price: price, side: 'buy', type: 'limit'}, _.bind(set, this)); +} + +Trader.prototype.sell = function(amount, price, callback) { + var set = function(err, result) { + if(err || result.error) + return log.error('unable to sell:', err, result); + + callback(null, result.serverOrderId); + }; + + this.mexbt.createOrder({amount: amount, price: price, side: 'sell', type: 'limit'}, _.bind(set, this)); +} + +Trader.prototype.checkOrder = function(order, callback) { + var currentPair = this.pair; + + var check = function(err, result) { + var ordersForPair = _.find(result, function(o) { return o.ins === currentPair}); + var stillThere = _.find(ordersForPair.openOrders, function(o) { return o.ServerOrderId === order }); + callback(err, !stillThere); + }; + + this.mexbt.accountOrders(_.bind(check, this)); +} + +Trader.prototype.cancelOrder = function(order, callback) { + var cancel = function(err, result) { + if(err || !result) + log.error('unable to cancel order', order, '(', err, result, ')'); + }; + + this.mexbt.cancelOrder({id: order}, _.bind(cancel, this)); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) + return this.retry(this.getTrades, args); + trades = _.map(result.trades, function (t) { + return {tid: t.tid, price: t.px, date: t.unixtime, amount: t.qty}; + }); + if (descending) { + trades = trades.reverse() + } + callback(null, trades); + }.bind(this); + + var endDate = moment().unix(); + + // FIXME: there is a bug in meXBT tradesByDate function, that it doesnt return all data + // when trying to fetch all. + // So if no since, we just fetch all via trades and giving a high count + if (since) { + this.mexbt.tradesByDate({startDate: since.unix(), endDate: endDate}, process); + } else { + // improvised + this.mexbt.trades({count: 1000}, process); + } +} + + +module.exports = Trader; diff --git a/exchanges/okcoin.js b/exchanges/okcoin.js new file mode 100644 index 000000000..a20c2d2dd --- /dev/null +++ b/exchanges/okcoin.js @@ -0,0 +1,163 @@ +var OKCoin = require('okcoin-china'); +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + this.clientID = config.username; + } + this.pair = [config.asset, config.currency].join('_').toLowerCase(); + this.name = 'okcoin'; + this.okcoin = new OKCoin(this.key, this.secret); + this.lastTid = false; +} + +// if the exchange errors we try the same call again after +// waiting 10 seconds +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) + }, + wait + ); +} + +Trader.prototype.getPortfolio = function(callback) { + var calculate = function(err, data) { + if(err) { + if(err.message === 'invalid api key') + util.die('Your ' + this.name + ' API keys are invalid'); + return this.retry(this.okcoin.getUserInfo, calculate); + } + + var portfolio = []; + _.each(data.info.funds.free, function(amount, asset) { + portfolio.push({name: asset.toUpperCase(), amount: +amount}); + }); + + callback(err, portfolio); + }.bind(this); + + this.okcoin.getUserInfo(calculate); +} + +Trader.prototype.getTicker = function(callback) { + var args = [this.pair, process]; + var process = function(err, data) { + if (err) + return this.retry(this.okcoin.getTicker(args)); + + var ticker = _.extend(data.ticker, { + bid: +data.ticker.sell, + ask: +data.ticker.buy + }); + + callback(err, ticker); + }.bind(this); + + this.okcoin.getTicker(process, args); +} + +// This assumes that only limit orders are being placed, so fees are the +// "maker fee" of 0.1%. It does not take into account volume discounts. +Trader.prototype.getFee = function(callback) { + var makerFee = 0.1; + callback(false, makerFee / 100); +} + +Trader.prototype.buy = function(raw_amount, price, callback) { + var amount = Math.floor(raw_amount * 10000) / 10000 + var set = function(err, result) { + if(err) + return log.error('unable to process order:', err, result); + + callback(null, result.order_id); + }.bind(this); + + this.okcoin.addTrade(set, this.pair, 'buy', amount, price); +} + +Trader.prototype.sell = function(raw_amount, price, callback) { + var amount = Math.floor(raw_amount * 10000) / 10000 + var set = function(err, result) { + if(err) + return log.error('unable to process order:', err, result); + + callback(null, result.order_id); + }.bind(this); + + this.okcoin.addTrade(set, this.pair, 'sell', amount, price); +} + + +Trader.prototype.checkOrder = function(order_id, callback) { + var check = function(err, result) { + if(err || !result.result) { + log.error('Perhaps the order already got filled?', '(', result, ')'); + callback(err, !result.result); + } else { + // output successful trade + var color = { + sell: '\x1b[31mSOLD\x1b[0m ', + buy: '\x1b[32mBOUGHT\x1b[0m ', + } + + log.info(color[result.orders[0].type], + result.orders[0].amount, ' @ ', result.orders[0].price); + } + } + + this.okcoin.getOrderInfo(check, this.pair, order_id); +} + +Trader.prototype.cancelOrder = function(order_id, callback) { + var cancel = function(err, result) { + if(err || !result.result) { + log.error('unable to cancel order ', order_id, '(', result, ')'); + } + }.bind(this); + + this.okcoin.cancelOrder(cancel, this.pair, order_id); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + + this.okcoin.getTrades(function(err, data) { + if (err) + return this.retry(this.getTrades, args); + + var trades = _.map(data, function(trade) { + return { + price: +trade.price, + amount: +trade.amount, + tid: +trade.tid, + date: trade.date + } + }); + + callback(null, trades.reverse()); + }.bind(this), this.pair, since); +} + +module.exports = Trader; \ No newline at end of file diff --git a/exchanges/poloniex.js b/exchanges/poloniex.js new file mode 100644 index 000000000..3a7faa4e2 --- /dev/null +++ b/exchanges/poloniex.js @@ -0,0 +1,166 @@ +var Poloniex = require("poloniex.js"); +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +// Helper methods +function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; +} + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + this.currency = config.currency; + this.asset = config.asset; + } + this.name = 'Poloniex'; + this.balance; + this.price; + + this.pair = [this.currency, this.asset].join('_'); + + this.poloniex = new Poloniex(this.key, this.secret); +} + +// if the exchange errors we try the same call again after +// waiting 10 seconds +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) }, + wait + ); +} + +Trader.prototype.getPortfolio = function(callback) { + var args = _.toArray(arguments); + var set = function(err, data) { + if(err) + return this.retry(this.getPortfolio, args); + + var portfolio = []; + _.each(data, function(amount, asset) { + portfolio.push({name: asset, amount: parseFloat(amount)}); + }); + + callback(err, portfolio); + }.bind(this); + + this.poloniex.myBalances(set); +} + +Trader.prototype.getTicker = function(callback) { + var args = _.toArray(arguments); + this.poloniex.getTicker(function(err, data) { + if(err) + return this.retry(this.getTicker, args); + + var tick = data[this.pair]; + + callback(null, { + bid: parseFloat(tick.highestBid), + ask: parseFloat(tick.lowestAsk), + }); + + }.bind(this)); +} + +Trader.prototype.getFee = function(callback) { + var set = function(err, data) { + if(err) + callback(err); + + callback(false, parseFloat(data.takerFee)); + } + this.poloniex._private('returnFeeInfo', _.bind(set, this)); +} + +Trader.prototype.buy = function(amount, price, callback) { + var set = function(err, result) { + if(err || result.error) + return log.error('unable to buy:', err, result); + + callback(null, result.orderNumber); + }.bind(this); + + this.poloniex.buy(this.currency, this.asset, price, amount, set); +} + +Trader.prototype.sell = function(amount, price, callback) { + var set = function(err, result) { + if(err || result.error) + return log.error('unable to sell:', err, result); + + callback(null, result.orderNumber); + }.bind(this); + + this.poloniex.sell(this.currency, this.asset, price, amount, set); +} + +Trader.prototype.checkOrder = function(order, callback) { + var check = function(err, result) { + var stillThere = _.find(result, function(o) { return o.orderNumber === order }); + callback(err, !stillThere); + }.bind(this); + + this.poloniex.myOpenOrders(this.currency, this.asset, check); +} + +Trader.prototype.cancelOrder = function(order, callback) { + var cancel = function(err, result) { + if(err || !result.success) { + log.error('unable to cancel order', order, '(', err, result, ')'); + // return this.retry(this.cancelOrder, args); + } + }.bind(this); + + this.poloniex.cancelOrder(this.currency, this.asset, order, cancel); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) { + return this.retry(this.getTrades, args); + } + + result = _.map(result, function(trade) { + return { + tid: trade.tradeID, + amount: +trade.amount, + date: moment.utc(trade.date).format('X'), + price: +trade.rate + }; + }); + + callback(null, result.reverse()); + }; + + var params = { + currencyPair: joinCurrencies(this.currency, this.asset) + } + if (since) + params.start = _.isNumber(since) ? since : since.format('X'); + + this.poloniex._public('returnTradeHistory', params, _.bind(process, this)); +} + + +module.exports = Trader; diff --git a/exchanges/zaif.jp.js b/exchanges/zaif.jp.js new file mode 100644 index 000000000..58c27a5c2 --- /dev/null +++ b/exchanges/zaif.jp.js @@ -0,0 +1,127 @@ +var Zaif = require("zaif.jp"); +var util = require('../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../core/log'); + +var Trader = function(config) { + _.bindAll(this); + if(_.isObject(config)) { + this.key = config.key; + this.secret = config.secret; + this.clientID = config.username; + } + this.name = 'Zaif'; + this.balance; + this.price; + + this.zaif = Zaif.createPrivateApi(this.key, this.secret, 'user agent is node-zaif'); + this.api = Zaif.PublicApi; +} + +// if the exchange errors we try the same call again after +// waiting 10 seconds +Trader.prototype.retry = function(method, args) { + var wait = +moment.duration(10, 'seconds'); + log.debug(this.name, 'returned an error, retrying..'); + + var self = this; + + // make sure the callback (and any other fn) + // is bound to Trader + _.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) }, + wait + ); +} + +Trader.prototype.getPortfolio = function(callback) { + var set = function(data) { + var portfolio = []; + _.each(data.funds, function(amount, asset) { +// if(asset.indexOf('available') !== -1) { + asset = asset.substr(0, 3).toUpperCase(); + portfolio.push({name: asset, amount: parseFloat(amount)}); +// } + }); + callback(err, portfolio); + } + this.zaif.getInfo().then(_.bind(set, this)); +} + +Trader.prototype.getTicker = function(callback) { + this.api.ticker('btc_jpy').then(callback); +} + +Trader.prototype.getFee = function(callback) { + var makerFee = 0.0; + callback(false, makerFee / 100); +} + +Trader.prototype.buy = function(amount, price, callback) { + var set = function(result) { + if(!result) + return log.error('unable to buy:', result); + + callback(null, result.order_id); + }; + + // TODO: fees are hardcoded here? +// amount *= 0.995; // remove fees + // prevent: Ensure that there are no more than 8 digits in total. + amount *= 100000000; + amount = Math.floor(amount); + amount /= 100000000; + this.zaif.trade('bid', price, amount).then(_.bind(set, this)); +} + +Trader.prototype.sell = function(amount, price, callback) { + var set = function(result) { + if(!result) + return log.error('unable to sell:', result); + + callback(null, result.order_id); + }; + + this.zaif.trade('ask', price, amount).then(_.bind(set, this)); +} + +Trader.prototype.checkOrder = function(order, callback) { + var check = function(result) { + var stillThere = (order in result); + callback(null, !stillThere); + }; + + this.zaif.activeorders().then(_.bind(check, this)); +} + +Trader.prototype.cancelOrder = function(order, callback) { + var cancel = function(result) { + if(!result) + log.error('unable to cancel order', order, '(', result, ')'); + }; + + this.zaif.cancelorder(order).then(_.bind(cancel, this)); +} + +Trader.prototype.getTrades = function(since, callback, descending) { + var args = _.toArray(arguments); + var process = function(result) { + if(!result) + return this.retry(this.getTrades, args); + + callback(null, result.reverse()); + }; + + this.api.trades('btc_jpy').then(_.bind(process, this)); +} + + +module.exports = Trader; \ No newline at end of file diff --git a/importers/exchanges/btcc.js b/importers/exchanges/btcc.js new file mode 100644 index 000000000..730fe8bb4 --- /dev/null +++ b/importers/exchanges/btcc.js @@ -0,0 +1,89 @@ +var BTCChina = require('btc-china-fork'); +var util = require('../../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../../core/log'); + +var config = util.getConfig(); + +var dirs = util.dirs(); + +var Fetcher = require(dirs.exchanges + 'btcc'); + +// patch getTrades.. +Fetcher.prototype.getTrades = function(fromTid, sinceTime, callback) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err) + return this.retry(this.getTrades, args); + + callback(result); + }.bind(this); + + if(sinceTime) + var params = { + limit: 1, + sincetype: 'time', + since: sinceTime + } + + else if(fromTid) + var params = { + limit: 5000, + since: fromTid + } + + this.btcc.getHistoryData(process, params); +} + +util.makeEventEmitter(Fetcher); + +var iterator = false; +var end = false; +var done = false; +var from = false; + +var fetcher = new Fetcher(config.watch); + +var fetch = () => { + if(!iterator) + fetcher.getTrades(false, from, handleFirstFetch); + else + fetcher.getTrades(iterator, false, handleFetch); +} + +// we use the first fetch to figure out +// the tid of the moment we want data from +var handleFirstFetch = trades => { + iterator = _.first(trades).tid; + fetch(); +} + +var handleFetch = trades => { + + iterator = _.last(trades).tid; + var last = moment.unix(_.last(trades).date); + + if(last > end) { + fetcher.emit('done'); + + var endUnix = end.unix(); + trades = _.filter( + trades, + t => t.date <= endUnix + ); + } + + fetcher.emit('trades', trades); +} + +module.exports = function (daterange) { + from = daterange.from.unix(); + end = daterange.to.clone(); + + return { + bus: fetcher, + fetch: fetch + } +} + diff --git a/importers/exchanges/poloniex.js b/importers/exchanges/poloniex.js new file mode 100644 index 000000000..a8fd71785 --- /dev/null +++ b/importers/exchanges/poloniex.js @@ -0,0 +1,110 @@ +var Poloniex = require("poloniex.js"); +var util = require('../../core/util.js'); +var _ = require('lodash'); +var moment = require('moment'); +var log = require('../../core/log'); + +var config = util.getConfig(); + +var dirs = util.dirs(); + +var Fetcher = require(dirs.exchanges + 'poloniex'); + +var batchSize = 60 * 2; // 2 hour +var overlapSize = 10; // 10 minutes + +// Helper methods +function joinCurrencies(currencyA, currencyB){ + return currencyA + '_' + currencyB; +} + +// patch getTrades.. +Fetcher.prototype.getTrades = function(range, callback) { + var args = _.toArray(arguments); + var process = function(err, result) { + if(err || result.error) + return this.retry(this.getTrades, args); + + if(_.size(result) === 50000) { + // to many trades.. + util.die('too many trades..'); + } + + result = _.map(result, function(trade) { + return { + tid: trade.tradeID, + amount: +trade.amount, + date: moment.utc(trade.date).format('X'), + price: +trade.rate + }; + }); + + callback(result.reverse()); + }.bind(this); + + var params = { + currencyPair: joinCurrencies(this.currency, this.asset) + } + + params.start = range.from.unix(); + params.end = range.to.unix(); + + this.poloniex._public('returnTradeHistory', params, process); +} + +util.makeEventEmitter(Fetcher); + +var iterator = false; +var end = false; +var done = false; + +var fetcher = new Fetcher(config.watch); + +var fetch = () => { + log.debug( + 'Requesting data from', + iterator.from.format('YYYY-MM-DD HH:mm:ss') + ',', + 'to', + iterator.to.format('YYYY-MM-DD HH:mm:ss') + ); + fetcher.getTrades(iterator, handleFetch); +} + +var handleFetch = trades => { + + iterator.from.add(batchSize, 'minutes').subtract(overlapSize, 'minutes'); + iterator.to.add(batchSize, 'minutes').subtract(overlapSize, 'minutes'); + + if(!_.size(trades)) + return fetcher.emit('trades', []); + + var last = moment.unix(_.last(trades).date); + + if(last > end) { + fetcher.emit('done'); + + var endUnix = end.unix(); + trades = _.filter( + trades, + t => t.date <= endUnix + ); + } + + fetcher.emit('trades', trades); +} + +module.exports = function (daterange) { + iterator = { + from: daterange.from.clone(), + to: daterange.from.clone().add(batchSize, 'minutes') + } + end = daterange.to.clone(); + + return { + bus: fetcher, + fetch: fetch + } +} + + + diff --git a/methods/Alligator.js b/methods/Alligator.js new file mode 100644 index 000000000..7a68c1806 --- /dev/null +++ b/methods/Alligator.js @@ -0,0 +1,136 @@ +// helpers +var _ = require('lodash'); +var log = require('../core/log.js'); + +// configuration +var config = require('../core/util.js').getConfig(); +var settings = config.CCI; +var pposettings = config.PPO; + + +// let's create our own method +var method = {}; + +// teach our trading method events +var Util = require('util'); + +// prepare everything our method needs +method.init = function() { + this.currentTrend; + this.requiredHistory = config.tradingAdvisor.historySize; + + this.age = 0; + this.trend = { + direction: 'undefined', + duration: 0, + persisted: false, + adviced: false + }; + this.historySize = config.tradingAdvisor.historySize; + this.uplevel = config.CCI.thresholds.up; + this.downlevel = config.CCI.thresholds.down; + this.persisted = config.CCI.thresholds.persistence; + + // log.debug("CCI started with:\nup:\t", this.uplevel, "\ndown:\t", this.downlevel, "\npersistence:\t", this.persisted); + // define the indicators we need + this.addIndicator('alli', 'Alligator', config.Alligator); +} + +// what happens on every new candle? +method.update = function(candle) { +} + +// for debugging purposes: log the last calculated +// EMAs and diff. +method.log = function() { + var alli = this.indicators.alli; + if (typeof(alli.result) == 'boolean') { + log.debug('Insufficient data available. Age: ', alli.size, ' of ', alli.maxSize); +// log.debug('ind: ', cci.TP.result, ' ', cci.TP.age, ' ', cci.TP.depth); + return; + } + + log.debug('calculated Alligator state for candle:'); + log.debug('\t', 'Price:\t\t\t', this.lastPrice); + log.debug('\t', 'SSMAJaws:\t', alli.SSMAJaws.toFixed(8)); + log.debug('\t', 'SSMATeeth:\t', alliSSMA.Teeth.toFixed(8)); + log.debug('\t', 'SSMALips/n:\t', alli.SSMALips.toFixed(8)); + if (typeof(cci.result) == 'boolean' ) + log.debug('\t In sufficient data available.'); + else + log.debug('\t', 'Alli:\t', cci.result.toFixed(2)); +} + +/* + * + */ +method.check = function() { + + + var price = this.lastPrice; + + + this.age++; + var alli = this.indicators.alli; + + if (typeof(alli.result) == 'number') { + + // overbought? + if (alli.result >= this.uplevel && (this.trend.persisted || this.persisted == 0) && !this.trend.adviced && this.trend.direction == 'overbought' ) { + this.trend.adviced = true; + this.trend.duration++; + this.advice('short'); + } else if (alli.result >= this.uplevel && this.trend.direction != 'overbought') { + this.trend.duration = 1; + this.trend.direction = 'overbought'; + this.trend.persisted = false; + this.trend.adviced = false; + if (this.persisted == 0) { + this.trend.adviced = true; + this.advice('short'); + } + } else if (alli.result >= this.uplevel) { + this.trend.duration++; + if (this.trend.duration >= this.persisted) { + this.trend.persisted = true; + } + } else if (alli.result <= this.downlevel && (this.trend.persisted || this.persisted == 0) && !this.trend.adviced && this.trend.direction == 'oversold') { + this.trend.adviced = true; + this.trend.duration++; + this.advice('long'); + } else if (alli.result <= this.downlevel && this.trend.direction != 'oversold') { + this.trend.duration = 1; + this.trend.direction = 'oversold'; + this.trend.persisted = false; + this.trend.adviced = false; + if (this.persisted == 0) { + this.trend.adviced = true; + this.advice('long'); + } + } else if (alli.result <= this.downlevel) { + this.trend.duration++; + if (this.trend.duration >= this.persisted) { + this.trend.persisted = true; + } + } else { + if( this.trend.direction != 'nodirection') { + this.trend = { + direction: 'nodirection', + duration: 0, + persisted: false, + adviced: false + }; + } else { + this.trend.duration++; + } + this.advice(); + } + + } else { + this.advice(); + } + + log.debug("Trend: ", this.trend.direction, " for ", this.trend.duration); +} + +module.exports = method; diff --git a/methods/CCI.js b/methods/CCI.js new file mode 100644 index 000000000..3b58e4f01 --- /dev/null +++ b/methods/CCI.js @@ -0,0 +1,140 @@ +// helpers +var _ = require('lodash'); +var log = require('../core/log.js'); + +// configuration +var config = require('../core/util.js').getConfig(); +var settings = config.CCI; +var pposettings = config.PPO; + + +// let's create our own method +var method = {}; + +// teach our trading method events +var Util = require('util'); +//var EventEmitter = require('events').EventEmitter; +//Util.inherits(TradingMethod, EventEmitter); + + +// prepare everything our method needs +method.init = function() { + this.currentTrend; + this.requiredHistory = config.tradingAdvisor.historySize; + + this.age = 0; + this.trend = { + direction: 'undefined', + duration: 0, + persisted: false, + adviced: false + }; + this.historySize = config.tradingAdvisor.historySize; + this.ppoadv = 'none'; + this.uplevel = config.CCI.thresholds.up; + this.downlevel = config.CCI.thresholds.down; + this.persisted = config.CCI.thresholds.persistence; + + // log.debug("CCI started with:\nup:\t", this.uplevel, "\ndown:\t", this.downlevel, "\npersistence:\t", this.persisted); + // define the indicators we need + this.addIndicator('cci', 'CCI', config.CCI); +} + +// what happens on every new candle? +method.update = function(candle) { +} + +// for debugging purposes: log the last calculated +// EMAs and diff. +method.log = function() { + var cci = this.indicators.cci; + if (typeof(cci.result) == 'boolean') { + log.debug('Insufficient data available. Age: ', cci.size, ' of ', cci.maxSize); + log.debug('ind: ', cci.TP.result, ' ', cci.TP.age, ' ', cci.TP.depth); + return; + } + + log.debug('calculated CCI properties for candle:'); + log.debug('\t', 'Price:\t\t\t', this.lastPrice); + log.debug('\t', 'CCI tp:\t', cci.tp.toFixed(8)); + log.debug('\t', 'CCI tp/n:\t', cci.TP.result.toFixed(8)); + log.debug('\t', 'CCI md:\t', cci.mean.toFixed(8)); + if (typeof(cci.result) == 'boolean' ) + log.debug('\t In sufficient data available.'); + else + log.debug('\t', 'CCI:\t', cci.result.toFixed(2)); +} + +/* + * + */ +method.check = function() { + + + var price = this.lastPrice; + + + this.age++; + var cci = this.indicators.cci; + + if (typeof(cci.result) == 'number') { + + // overbought? + if (cci.result >= this.uplevel && (this.trend.persisted || this.persisted == 0) && !this.trend.adviced && this.trend.direction == 'overbought' ) { + this.trend.adviced = true; + this.trend.duration++; + this.advice('short'); + } else if (cci.result >= this.uplevel && this.trend.direction != 'overbought') { + this.trend.duration = 1; + this.trend.direction = 'overbought'; + this.trend.persisted = false; + this.trend.adviced = false; + if (this.persisted == 0) { + this.trend.adviced = true; + this.advice('short'); + } + } else if (cci.result >= this.uplevel) { + this.trend.duration++; + if (this.trend.duration >= this.persisted) { + this.trend.persisted = true; + } + } else if (cci.result <= this.downlevel && (this.trend.persisted || this.persisted == 0) && !this.trend.adviced && this.trend.direction == 'oversold') { + this.trend.adviced = true; + this.trend.duration++; + this.advice('long'); + } else if (cci.result <= this.downlevel && this.trend.direction != 'oversold') { + this.trend.duration = 1; + this.trend.direction = 'oversold'; + this.trend.persisted = false; + this.trend.adviced = false; + if (this.persisted == 0) { + this.trend.adviced = true; + this.advice('long'); + } + } else if (cci.result <= this.downlevel) { + this.trend.duration++; + if (this.trend.duration >= this.persisted) { + this.trend.persisted = true; + } + } else { + if( this.trend.direction != 'nodirection') { + this.trend = { + direction: 'nodirection', + duration: 0, + persisted: false, + adviced: false + }; + } else { + this.trend.duration++; + } + this.advice(); + } + + } else { + this.advice(); + } + + log.debug("Trend: ", this.trend.direction, " for ", this.trend.duration); +} + +module.exports = method; diff --git a/methods/DEMA.js b/methods/DEMA.js index 355d05bf2..b065c8ab5 100644 --- a/methods/DEMA.js +++ b/methods/DEMA.js @@ -11,6 +11,8 @@ var method = {}; // prepare everything our method needs method.init = function() { + this.name = 'DEMA'; + this.currentTrend; this.requiredHistory = config.tradingAdvisor.historySize; @@ -38,7 +40,6 @@ method.log = function() { } method.check = function() { - var dema = this.indicators.dema; var diff = dema.result; var price = this.lastPrice; diff --git a/methods/MACD.js b/methods/MACD.js index 8efeb3fab..d590097df 100644 --- a/methods/MACD.js +++ b/methods/MACD.js @@ -39,7 +39,6 @@ method.init = function() { // define the indicators we need this.addIndicator('macd', 'MACD', settings); - } // what happens on every new candle? @@ -65,13 +64,7 @@ method.log = function() { } method.check = function() { - var price = this.lastPrice; - var macd = this.indicators.macd; - - var long = macd.long.result; - var short = macd.short.result; - var signal = macd.signal.result; - var macddiff = macd.result; + var macddiff = this.indicators.macd.result; if(macddiff > settings.thresholds.up) { diff --git a/methods/RSI.js b/methods/RSI.js index 9a6754c38..743a9226e 100644 --- a/methods/RSI.js +++ b/methods/RSI.js @@ -19,6 +19,8 @@ var method = {}; // prepare everything our method needs method.init = function() { + this.name = 'RSI'; + this.trend = { direction: 'none', duration: 0, diff --git a/methods/StochRSI.js b/methods/StochRSI.js new file mode 100644 index 000000000..b25f1d083 --- /dev/null +++ b/methods/StochRSI.js @@ -0,0 +1,124 @@ +/* + + StochRSI - SamThomp 11/06/2014 + + (updated by askmike) @ 30/07/2016 + + */ +// helpers +var _ = require('lodash'); +var log = require('../core/log.js'); + +var config = require('../core/util.js').getConfig(); +var settings = config.StochRSI; + +var RSI = require('./indicators/RSI.js'); + +// let's create our own method +var method = {}; + +// prepare everything our method needs +method.init = function() { + this.interval = settings.interval; + + this.trend = { + direction: 'none', + duration: 0, + persisted: false, + adviced: false + }; + + this.requiredHistory = config.tradingAdvisor.historySize; + + // define the indicators we need + this.addIndicator('rsi', 'RSI', this.interval); + + this.RSIhistory = []; +} + +// what happens on every new candle? +method.update = function(candle) { + this.rsi = this.indicators.rsi.rsi; + + this.RSIhistory.push(this.rsi); + + if(_.size(this.RSIhistory) > this.interval) + // remove oldest RSI value + this.RSIhistory.shift(); + + this.lowestRSI = _.min(this.RSIhistory); + this.highestRSI = _.max(this.RSIhistory); + this.stochRSI = ((this.rsi - this.lowestRSI) / (this.highestRSI - this.lowestRSI)) * 100; +} + +// for debugging purposes log the last +// calculated parameters. +method.log = function() { + var digits = 8; + + log.debug('calculated StochRSI properties for candle:'); + log.debug('\t', 'rsi:', this.rsi.toFixed(digits)); + log.debug("StochRSI min:\t\t" + this.lowestRSI.toFixed(digits)); + log.debug("StochRSI max:\t\t" + this.highestRSI.toFixed(digits)); + log.debug("StochRSI Value:\t\t" + this.stochRSI.toFixed(2)); +} + +method.check = function() { + if(this.stochRSI > settings.thresholds.high) { + // new trend detected + if(this.trend.direction !== 'high') + this.trend = { + duration: 0, + persisted: false, + direction: 'high', + adviced: false + }; + + this.trend.duration++; + + log.debug('In high since', this.trend.duration, 'candle(s)'); + + if(this.trend.duration >= settings.thresholds.persistence) + this.trend.persisted = true; + + if(this.trend.persisted && !this.trend.adviced) { + this.trend.adviced = true; + this.advice('short'); + } else + this.advice(); + + } else if(this.stochRSI < settings.thresholds.low) { + + // new trend detected + if(this.trend.direction !== 'low') + this.trend = { + duration: 0, + persisted: false, + direction: 'low', + adviced: false + }; + + this.trend.duration++; + + log.debug('In low since', this.trend.duration, 'candle(s)'); + + if(this.trend.duration >= settings.thresholds.persistence) + this.trend.persisted = true; + + if(this.trend.persisted && !this.trend.adviced) { + this.trend.adviced = true; + this.advice('long'); + } else + this.advice(); + + } else { + // trends must be on consecutive candles + this.trend.duration = 0; + log.debug('In no trend'); + + this.advice(); + } + +} + +module.exports = method; \ No newline at end of file diff --git a/methods/custom.js b/methods/custom.js index 84bf11c37..82c85f7b7 100644 --- a/methods/custom.js +++ b/methods/custom.js @@ -2,7 +2,7 @@ // write them here. For more information on everything you // can use please refer to this document: // -// https://github.com/askmike/gekko/blob/master/docs/trading_methods.md +// https://github.com/askmike/gekko/blob/stable/docs/trading_methods.md // // The example below is pretty stupid: on every new candle there is // a 10% chance it will recommand to change your position (to either diff --git a/methods/indicators/Alligator.js b/methods/indicators/Alligator.js new file mode 100644 index 000000000..84ee2fd4e --- /dev/null +++ b/methods/indicators/Alligator.js @@ -0,0 +1,116 @@ +var log = require('../../core/log'); + +var Indicator = function(settings) { + this.depth = settings.history; + this.result = false; + this.age = 0; + //this.history = []; + this.high = []; + this.low = []; + this.x = []; + + this.SSMAJaws = 0; + this.SSMATeeth = 0; + this.SSMALips = 0; + + /* + * Do not use array(depth) as it might not be implemented + */ + for (var i = 0; i < this.depth; i++) { + //this.history.push(0.0); + this.high.push(0.0); + this.low.push(0.0); + + this.x.push(i); + } + + log.debug("Created Alligator indicator with h: ", this.depth); +} + +Indicator.prototype.update = function(candle) { + + // We need sufficient history to get the right result. + if(this.result === false && this.age < this.depth) { + + //this.history[this.age] = price; + this.high[this.age] = candle.high; + this.low[this.age] = candle.low; + + this.age++; + this.result = false; + log.debug("Waiting for sufficient age: ", this.age, " out of ", this.depth); + // + return; + } + + this.age++; + // shift history + for (var i = 0; i < (this.depth - 1); i++) { + //this.history[i] = this.history[i+1]; + this.high[i] = this.high[i + 1]; + this.low[i] = this.low[i + 1]; + } + + //this.history[this.depth-1] = price; + this.high[this.depth-1] = candle.high; + this.low[this.depth-1] = candle.low; + + this.calculate(candle); + return; + } + +* +* Handle calculations +*/ +Indicator.prototype.calculate = function(candle) { +//Bill Williams Alligator for Think or Swim +//Mike Lapping 2010 +//Extra steps were taken because I cannot find proper +//documentation of the average() function +//can be used and modified by anyone for any reason. do not sell. + + // We will now work on the SSMA called jaws + var JOffset = 8; + var Javg13 = ((this.high[12 + JOffset] + this.low[12 + JOffset]) / 2); + var Javg12 = ((this.high[11 + JOffset] + this.low[11 + JOffset]) / 2); + var Javg11 = ((this.high[10 + JOffset] + this.low[10 + JOffset]) / 2); + var Javg10 = ((this.high[9 + JOffset] + this.low[9 + JOffset]) / 2); + var Javg9 = ((this.high[8 + JOffset] + this.low[8 + JOffset]) / 2); + var Javg8 = ((this.high[7 + JOffset] + this.low[7 + JOffset]) / 2); + var Javg7 = ((this.high[6 + JOffset] + this.low[6 + JOffset]) / 2); + var Javg6 = ((this.high[5 + JOffset] + this.low[5 + JOffset]) / 2); + var Javg5 = ((this.high[4 + JOffset] + this.low[4 + JOffset]) / 2); + var Javg4 = ((this.high[3 + JOffset] + this.low[3 + JOffset]) / 2); + var Javg3 = ((this.high[2 + JOffset] + this.low[2 + JOffset]) / 2); + var Javg2 = ((this.high[1 + JOffset] + this.low[1 + JOffset]) / 2); + var Javg1 = ((this.high[0 + JOffset] + this.low[0 + JOffset]) / 2); + + this.SSMAJaws = ( Javg1 + Javg2 + Javg3 + Javg4 + Javg5 + Javg6 + Javg7 + Javg8 + Javg9 + Javg10 + Javg11 + Javg12 + Javg13) / 13; + +//Now working on Lips + var LOffset = 5; + var Lavg8 = ((this.high[7 + LOffset] + this.low[7 + LOffset]) / 2); + var Lavg7 = ((this.high[6 + LOffset] + this.low[6 + LOffset]) / 2); + var Lavg6 = ((this.high[5 + LOffset] + this.low[5 + LOffset]) / 2); + var Lavg5 = ((this.high[4 + LOffset] + this.low[4 + LOffset]) / 2); + var Lavg4 = ((this.high[3 + LOffset] + this.low[3 + LOffset]) / 2); + var Lavg3 = ((this.high[2 + LOffset] + this.low[2 + LOffset]) / 2); + var Lavg2 = ((this.high[1 + LOffset] + this.low[1 + LOffset]) / 2); + var Lavg1 = ((this.high[0 + LOffset] + this.low[0 + LOffset]) / 2); + + this.SSMALips = (Lavg1 + Lavg2 + Lavg3 + Lavg4 + Lavg5 + Lavg6 + Lavg7 + Lavg8) / 8; + +//Work on teeth + var TOffset = 3; + + var Tavg5 = ((this.high[4 + TOffset] + this.low[4 + TOffset]) / 2); + var Tavg4 = ((this.high[3 + TOffset] + this.low[3 + TOffset]) / 2); + var Tavg3 = ((this.high[2 + TOffset] + this.low[2 + TOffset]) / 2); + var Tavg2 = ((this.high[1 + TOffset] + this.low[1 + TOffset]) / 2); + var Tavg1 = ((this.high[0 + TOffset] + this.low[0 + TOffset]) / 2); + + this.SSMATeeth = (Tavg1 + Tavg2 + Tavg3 + Tavg4 + Tavg5) / 5; + +} + +module.exports = Indicator; \ No newline at end of file diff --git a/methods/indicators/CCI.js b/methods/indicators/CCI.js new file mode 100644 index 000000000..20f6183c3 --- /dev/null +++ b/methods/indicators/CCI.js @@ -0,0 +1,78 @@ +/* + * CCI + */ +var log = require('../../core/log'); +var LRC = require('./LRC'); + +var Indicator = function(settings) { + this.tp = 0.0; + this.TP = new LRC(settings.history); + this.result = false; + this.hist = Array(); // needed for mean? + this.mean = 0.0; + this.size = 0; + this.constant = settings.constant; + this.maxSize = settings.history; + for (var i = 0; i < this.maxSize; i++) + this.hist.push(0.0); +} + +Indicator.prototype.update = function(candle) { + + // We need sufficient history to get the right result. + + var tp = (candle.high + candle.close + candle.low) / 3; + if (this.size < this.maxSize) { + this.hist[this.size] = tp; + this.size++; + } else { + for (var i = 0; i < this.maxSize-1; i++) { + this.hist[i] = this.hist[i+1]; + } + this.hist[this.maxSize-1] = tp; + } + + this.TP.update(tp); + + if (this.size < this.maxSize) { + this.result = false; + } else { + this.calculate(tp); + } +} + +/* + * Handle calculations + */ +Indicator.prototype.calculate = function(tp) { + + // calculate current TP + + var avgtp = this.TP.result; + if (typeof(avgtp) == 'boolean') { + log.error("Failed to get average tp from indicator."); + return; + } + + this.tp = tp; + + var sum = 0.0; + // calculate tps + for (var i = 0; i < this.size; i++) { + + var z = (this.hist[i] - avgtp); + if (z < 0) z = z * -1.0; + sum = sum + z; + + } + + this.mean = (sum / this.size); + + + + this.result = (this.tp - avgtp) / (this.constant * this.mean); + + // log.debug("===\t", this.mean, "\t", this.tp, '\t', this.TP.result, "\t", sum, "\t", avgtp, '\t', this.result.toFixed(2)); +} + +module.exports = Indicator; diff --git a/methods/indicators/LRC.js b/methods/indicators/LRC.js new file mode 100644 index 000000000..241a33200 --- /dev/null +++ b/methods/indicators/LRC.js @@ -0,0 +1,114 @@ +/* + * Lineair regression curve + */ +var log = require('../../core/log'); + +var Indicator = function(settings) { + this.depth = settings; + this.result = false; + this.age = 0; + this.history = []; + this.x = []; + /* + * Do not use array(depth) as it might not be implemented + */ + for (var i = 0; i < this.depth; i++) { + this.history.push(0.0); + this.x.push(i); + } + + // log.debug("Created LRC indicator with h: ", this.depth); +} + +Indicator.prototype.update = function(price) { + + // We need sufficient history to get the right result. + if(this.result === false && this.age < this.depth) { + + this.history[this.age] = price; + this.age++; + this.result = false; + // log.debug("Waiting for sufficient age: ", this.age, " out of ", this.depth); + // + return; + } + + this.age++; + // shift history + for (var i = 0; i < (this.depth - 1); i++) { + this.history[i] = this.history[i+1]; + } + this.history[this.depth-1] = price; + + this.calculate(price); + + + // log.debug("Checking LRC: ", this.result.toFixed(8), "\tH: ", this.age); + return; +} + +/* + * Least squares linear regression fitting. + */ +function linreg(values_x, values_y) { + var sum_x = 0; + var sum_y = 0; + var sum_xy = 0; + var sum_xx = 0; + var count = 0; + + /* + * We'll use those variables for faster read/write access. + */ + var x = 0; + var y = 0; + var values_length = values_x.length; + + if (values_length != values_y.length) { + throw new Error('The parameters values_x and values_y need to have same size!'); + } + + /* + * Nothing to do. + */ + if (values_length === 0) { + return [ [], [] ]; + } + + /* + * Calculate the sum for each of the parts necessary. + */ + for (var v = 0; v < values_length; v++) { + x = values_x[v]; + y = values_y[v]; + sum_x += x; + sum_y += y; + sum_xx += x*x; + sum_xy += x*y; + count++; + } + + /* + * Calculate m and b for the formular: + * y = x * m + b + */ + var m = (count*sum_xy - sum_x*sum_y) / (count*sum_xx - sum_x*sum_x); + var b = (sum_y/count) - (m*sum_x)/count; + + return [m, b]; +} + + +/* + * Handle calculations + */ +Indicator.prototype.calculate = function(price) { + + // get the reg + var reg = linreg(this.x, this.history); + + // y = a * x + b + this.result = ((this.depth-1) * reg[0]) + reg[1]; +} + +module.exports = Indicator; diff --git a/methods/indicators/RSI.js b/methods/indicators/RSI.js index fce925c23..c5ffe65c9 100644 --- a/methods/indicators/RSI.js +++ b/methods/indicators/RSI.js @@ -2,7 +2,7 @@ var EMA = require('./EMA.js'); var Indicator = function(weight) { - this.lastPrice = 0; + this.lastClose = 0; this.weight = weight; this.weightEma = 2 * weight - 1; this.avgU = new EMA(this.weightEma); @@ -15,15 +15,14 @@ var Indicator = function(weight) { } Indicator.prototype.update = function(candle) { - var open = candle.o; - var close = candle.c; + var currentClose = candle.close; - if(close > open) { - this.u = close - open; + if(currentClose > this.lastClose) { + this.u = currentClose - this.lastClose; this.d = 0; } else { this.u = 0; - this.d = open - close; + this.d = this.lastClose - currentClose; } this.avgU.update(this.u); @@ -32,6 +31,7 @@ Indicator.prototype.update = function(candle) { this.rsi = 100 - (100 / (1 + this.rs)); this.age++; + this.lastClose = currentClose; } module.exports = Indicator; diff --git a/methods/indicators/SMA.js b/methods/indicators/SMA.js index 2f5572deb..062f99f9e 100644 --- a/methods/indicators/SMA.js +++ b/methods/indicators/SMA.js @@ -1,5 +1,24 @@ -// required indicators +/** + * The first value for the Smoothed Moving Average is calculated as a Simple Moving Average (SMA): + +SUM1=SUM (CLOSE, N) + +SMMA1 = SUM1/ N + +The second and subsequent moving averages are calculated according to this formula: +SMMA (i) = (SUM1 – SMMA1+CLOSE (i))/ N + +Where: + +SUM1 – is the total sum of closing prices for N periods; +SMMA1 – is the smoothed moving average of the first bar; +SMMA (i) – is the smoothed moving average of the current bar (except the first one); +CLOSE (i) – is the current closing price; +N – is the smoothing period. + */ + +// required indicators var Indicator = function(weight) { this.weight = weight; this.prices = []; diff --git a/methods/talib-macd.js b/methods/talib-macd.js new file mode 100644 index 000000000..53c24ee44 --- /dev/null +++ b/methods/talib-macd.js @@ -0,0 +1,70 @@ +// If you want to use your own trading methods you can +// write them here. For more information on everything you +// can use please refer to this document: +// +// https://github.com/askmike/gekko/blob/stable/docs/trading_methods.md +// +// The example below is pretty stupid: on every new candle there is +// a 10% chance it will recommand to change your position (to either +// long or short). + +var config = require('../core/util.js').getConfig(); +var settings = config['talib-macd']; + +// Let's create our own method +var method = {}; + +// Prepare everything our method needs +method.init = function() { + this.name = 'talib-macd' + // keep state about the current trend + // here, on every new candle we use this + // state object to check if we need to + // report it. + this.trend = { + direction: 'none', + duration: 0, + persisted: false, + adviced: false + }; + + // how many candles do we need as a base + // before we can start giving advice? + this.requiredHistory = config.tradingAdvisor.historySize; + + var customMACDSettings = settings.parameters; + + // define the indicators we need + this.addTalibIndicator('macd', 'macd', customMACDSettings); +} + +// What happens on every new candle? +method.update = function(candle) { + // nothing! +} + + +method.log = function() { + // nothing! +} + +// Based on the newly calculated +// information, check if we should +// update or not. +method.check = function() { + var price = this.lastPrice; + var result = this.talibIndicators.macd.result; + var macddiff = result['outMACD'] - result['outMACDSignal']; + + if(settings.thresholds.down > macddiff && this.currentTrend !== 'short') { + this.currentTrend = 'short'; + this.advice('short'); + + } else if(settings.thresholds.up < macddiff && this.currentTrend !== 'long'){ + this.currentTrend = 'long'; + this.advice('long'); + + } +} + +module.exports = method; \ No newline at end of file diff --git a/plugins.js b/plugins.js index 979cab1e6..0538de5c5 100644 --- a/plugins.js +++ b/plugins.js @@ -1,62 +1,75 @@ -// what kind of actors does Gekko support? -// -// An actor is a module/plugin that acts whenever an event happens. -// In Gekko there are two types of events and each type originates -// from a feed: -// -// - Market Events: the market feed. -// - Advice Events: the advice feed. -// -// Each type has it's own feed. -// -// Required parameters per actor. -// -// name: Name of the actor -// slug: filename of the actor, expected to be in `gekko/actors/` -// description: text describing the actor. Unused on silent actors. -// async: upon creating a new actor instance, does something async +// All plugins supported by Gekko. +// +// Required parameters per plugin. +// +// name: Name of the plugin +// slug: name of the plugin mapped to the config key. Expected +// filename to exist in `gekko/plugins/` (only if path is not +// specified) +// async: upon creating a new plugin instance, does something async // happen where Gekko needs to wait for? If set to true, the // constructor will be passed a callback which it should execute -// as soon as Gekko can continue setup. -// silent: indicated whether Gekko should log when this actor is -// configured. Not neccesary for until components. -// modes: a list indicating in what Gekko modes this actor is +// as soon as Gekko can continue. +// modes: a list indicating in what Gekko modes this plugin is // allowed to run. Realtime is during a live market watch and // backtest is during a backtest. -// requires: a list of npm modules this actor requires to be -// installed. -// originates: does this actor originate a feed (internally used) -var actors = [ +// +// Optional parameters per plugin. +// +// description: text describing the plugin. +// dependencies: a list of external npm modules this plugin requires to +// be installed. +// emits: events emitted by this plugin that other plugins can subscribe to. +// path: fn that returns path of file of the plugin (overwrites `gekko/plugins/{slug}`) +// when given the configuration object (relative from `gekko/plugins/`). +var plugins = [ + { + name: 'Candle writer', + description: 'Store candles in a database', + slug: 'candleWriter', + async: true, + modes: ['realtime', 'importer'], + path: function(config) { + return config.adapter + '/writer'; + }, + version: 0.1, + }, { name: 'Trading Advisor', description: 'Calculate trading advice', slug: 'tradingAdvisor', - async: false, - silent: false, + async: true, modes: ['realtime', 'backtest'], - originates: [{ - feed: 'advice feed', - object: 'method' - }] + emits: ['advice'] }, { name: 'IRC bot', description: 'IRC module lets you communicate with Gekko on IRC.', slug: 'ircbot', async: false, - silent: false, modes: ['realtime'], dependencies: [{ module: 'irc', version: '0.3.6' }] }, + { + name: 'XMPP bot', + description: 'XMPP module lets you communicate with Gekko on Jabber.', + slug: 'xmppbot', + async: false, + silent: false, + modes: ['realtime'], + dependencies: [{ + module: 'node-xmpp', + version: '0.12.0' + }] + }, { name: 'Campfire bot', - description: 'Campfire module lets you communicate with Gekko on Campfire.', + description: 'Lets you communicate with Gekko on Campfire.', slug: 'campfire', async: false, - silent: false, modes: ['realtime'], dependencies: [{ module: 'ranger', @@ -65,14 +78,13 @@ var actors = [ }, { name: 'Mailer', - description: 'Mail module lets sends you email yourself everytime Gekko has new advice.', + description: 'Sends you an email everytime Gekko has new advice.', slug: 'mailer', async: true, - silent: false, modes: ['realtime'], dependencies: [{ module: 'emailjs', - version: '0.3.6' + version: '1.0.5' }, { module: 'prompt-lite', version: '0.1.1' @@ -104,10 +116,9 @@ var actors = [ }, { name: 'Trader', - description: 'Trader will follow the advice and create real orders.', + description: 'Follows the advice and create real orders.', slug: 'trader', async: true, - silent: false, modes: ['realtime'] }, { @@ -116,14 +127,13 @@ var actors = [ slug: 'adviceLogger', async: false, silent: true, - modes: ['realtime', 'backtest'] + modes: ['realtime'] }, { name: 'Profit Simulator', description: 'Paper trader that logs fake profits.', slug: 'profitSimulator', async: false, - silent: false, modes: ['realtime', 'backtest'] }, { @@ -131,7 +141,6 @@ var actors = [ slug: 'redisBeacon', description: 'Publish events over Redis Pub/Sub', async: true, - silent: false, modes: ['realtime'], dependencies: [{ module: 'redis', @@ -140,4 +149,4 @@ var actors = [ } ]; -module.exports = actors; +module.exports = plugins; diff --git a/plugins/campfire.js b/plugins/campfire.js index 28c03f83f..60eacee69 100644 --- a/plugins/campfire.js +++ b/plugins/campfire.js @@ -36,9 +36,11 @@ var Actor = function() { }; Actor.prototype = { - processTrade: function(trade) { - this.price = trade.price; - this.priceTime = Moment.unix(trade.date); + processCandle: function(candle, done) { + this.price = candle.close; + this.priceTime = candle.date.start(); + + done(); }, processAdvice: function(advice) { diff --git a/plugins/candleStore.js b/plugins/candleStore.js new file mode 100644 index 000000000..120c267f4 --- /dev/null +++ b/plugins/candleStore.js @@ -0,0 +1,28 @@ +// implement the neDB reader and writer + +var _ = require('lodash'); + +var util = require('../core/util.js'); +var config = util.getConfig(); + +var watch = config.watch; +var settings = { + exchange: watch.exchange, + pair: [watch.currency, watch.asset], + historyPath: config.history.directory +} + +var needsToRead = config.tradingAdvisor.enabled; + +var Writer = require('../core/datastores/nedb/candle-writer'); +if(needsToRead) + var Reader = require('../core/datastores/nedb/candle-reader') + +var Store = function() { + _.bindAll(this); + this.writer = new Writer(settings); +} + +Store.prototype.processCandle = function(candles) { + this.writer.write(candles); +} \ No newline at end of file diff --git a/plugins/ircbot.js b/plugins/ircbot.js index 27f671ace..c3be2b2bf 100644 --- a/plugins/ircbot.js +++ b/plugins/ircbot.js @@ -34,9 +34,11 @@ var Actor = function() { this.rawCommands = _.keys(this.commands); } -Actor.prototype.processTrade = function(trade) { - this.price = trade.price; - this.priceTime = moment.unix(trade.date); +Actor.prototype.processCandle = function(candle, done) { + this.price = candle.close; + this.priceTime = candle.start; + + done(); }; Actor.prototype.processAdvice = function(advice) { diff --git a/plugins/mailer.js b/plugins/mailer.js index f765e0ad3..f1dae9e2f 100644 --- a/plugins/mailer.js +++ b/plugins/mailer.js @@ -90,8 +90,10 @@ Mailer.prototype.mail = function(subject, content, done) { }, done || this.checkResults); } -Mailer.prototype.processTrade = function(trade) { - this.price = trade.price; +Mailer.prototype.processCandle = function(candle, done) { + this.price = candle.close; + + done(); } Mailer.prototype.processAdvice = function(advice) { diff --git a/plugins/profitSimulator.js b/plugins/profitSimulator.js index 1538e86cf..49122fa68 100644 --- a/plugins/profitSimulator.js +++ b/plugins/profitSimulator.js @@ -3,16 +3,22 @@ var _ = require('lodash'); var log = require('../core/log.js'); var moment = require('moment'); +var mode = util.gekkoMode(); + var config = util.getConfig(); var calcConfig = config.profitSimulator; var watchConfig = config.watch; var Logger = function() { - _.bindAll(this); - this.historySize = config.tradingAdvisor.historySize; - this.historyReceived = 0; + this.dates = { + start: false, + end: false + } + + this.startPrice = 0; + this.endPrice = 0; this.verbose = calcConfig.verbose; this.fee = 1 - (calcConfig.fee + calcConfig.slippage) / 100; @@ -54,13 +60,6 @@ Logger.prototype.round = function(amount) { return amount.toFixed(5); } -Logger.prototype.processTrade = function(trade) { - this.price = trade.price; - - if(!this.start.balance) - this.calculateStartBalance() -} - Logger.prototype.calculateStartBalance = function() { if(this.reportInCurrency) this.start.balance = this.start.currency + this.price * this.start.asset; @@ -75,12 +74,16 @@ Logger.prototype.processAdvice = function(advice) { this.tracks++; var what = advice.recommandation; + var time = this.dates.end.utc().format('YYYY-MM-DD HH:mm:ss') // virtually trade all USD to BTC at the current price if(what === 'long') { this.current.asset += this.extractFee(this.current.currency / this.price); this.current.currency = 0; this.trades++; + + if(mode === 'backtest') + log.info(`Profit simulator got advice to long\t@ ${time}, buying ${this.current.asset} ${this.asset} \t(${this.current.asset})`); } // virtually trade all BTC to USD at the current price @@ -88,22 +91,38 @@ Logger.prototype.processAdvice = function(advice) { this.current.currency += this.extractFee(this.current.asset * this.price); this.current.asset = 0; this.trades++; + + if(mode === 'backtest') + log.info(`Profit simulator got advice to short\t@ ${time}, selling ${this.current.asset} ${this.asset} \t(${this.current.currency})`); } - // without verbose we report after round trip (buy-sell) - if(!this.verbose && what === 'short') // && !config.backtest.enabled) + if(mode === 'realtime') this.report(); } -// with verbose we report after every new candle -if(calcConfig.verbose) - Logger.prototype.processCandle = function() { +Logger.prototype.processCandle = function(candle, done) { + if(!this.dates.start) { + this.dates.start = candle.start; + this.startPrice = candle.open; + } + + this.dates.end = candle.start.clone().add(1, 'm'); + this.endPrice = candle.close; + + this.price = candle.close; + + if(!this.start.balance) + this.calculateStartBalance(); + + if(!calcConfig.verbose) + return done(); + // skip on history if(++this.historyReceived < this.historySize) - return; + return done(); - this.report(); - } + done(); +} Logger.prototype.report = function(timespan) { if(!this.start.balance) @@ -117,96 +136,75 @@ Logger.prototype.report = function(timespan) { this.profit = this.current.balance - this.start.balance; this.relativeProfit = this.current.balance / this.start.balance * 100 - 100; - log.info( - '(PROFIT REPORT)', - 'original simulated balance:\t', - this.round(this.start.balance), - this.reportIn - ); - - log.info( - '(PROFIT REPORT)', - 'current simulated balance:\t', - this.round(this.current.balance), - this.reportIn - ); + var start = this.round(this.start.balance); + var current = this.round(this.current.balance); + log.info(`(PROFIT REPORT) original simulated balance:\t ${start} ${this.reportIn}`); + log.info(`(PROFIT REPORT) current simulated balance:\t ${current} ${this.reportIn}`); log.info( - '(PROFIT REPORT)', - 'simulated profit:\t\t', - this.round(this.profit), - this.reportIn, + `(PROFIT REPORT) simulated profit:\t\t ${this.round(this.profit)} ${this.reportIn}`, '(' + this.round(this.relativeProfit) + '%)' ); if(timespan) { - var timespanPerYear = 356 / timespan; - log.info( '(PROFIT REPORT)', 'simulated yearly profit:\t', - this.round(this.profit * timespanPerYear), + this.round(this.profit / timespan.asYears()), this.reportIn, - '(' + this.round(this.relativeProfit * timespanPerYear) + '%)' + '(' + this.round(this.relativeProfit / timespan.asYears()) + '%)' ); } } // finish up stats for backtesting -Logger.prototype.finish = function(data) { - console.log(); - console.log(); - - log.info('\tWARNING: BACKTESTING FEATURE NEEDS PROPER TESTING') - log.info('\tWARNING: ACT ON THESE NUMBERS AT YOUR OWN RISK!') - - console.log(); - console.log(); - - var start = moment.unix(data.startTime); - var end = moment.unix(data.endTime); - var timespan = end.diff(start, 'days'); +Logger.prototype.finalize = function() { + log.info('') log.info( '(PROFIT REPORT)', 'start time:\t\t\t', - start.format('YYYY-MM-DD HH:mm:ss') + this.dates.start.utc().format('YYYY-MM-DD HH:mm:ss') ); log.info( '(PROFIT REPORT)', 'end time:\t\t\t', - end.format('YYYY-MM-DD HH:mm:ss') + this.dates.end.utc().format('YYYY-MM-DD HH:mm:ss') + ); + + var timespan = moment.duration( + this.dates.end.diff(this.dates.start) ); log.info( '(PROFIT REPORT)', 'timespan:\t\t\t', - timespan, + timespan.humanize(), 'days' ); - console.log(); + log.info(); log.info( '(PROFIT REPORT)', 'start price:\t\t\t', - data.start + this.startPrice ); log.info( '(PROFIT REPORT)', 'end price:\t\t\t', - data.end + this.endPrice ); log.info( '(PROFIT REPORT)', 'Buy and Hold profit:\t\t', - this.round((data.end - data.start) / data.start * 100) + '%' + (this.round(this.endPrice * 100 / this.startPrice) - 100) + '%' ); - console.log(); + log.info(); log.info( '(PROFIT REPORT)', diff --git a/plugins/redisBeacon.js b/plugins/redisBeacon.js index 444ddbc41..b814bec19 100644 --- a/plugins/redisBeacon.js +++ b/plugins/redisBeacon.js @@ -38,11 +38,14 @@ _.each(redisBeacon.broadcast, function(e) { var channel = redisBeacon.channelPrefix + subscription.event - proto[subscription.handler] = function(message) { + proto[subscription.handler] = function(message, cb) { + if(!_.isFunction(cb)) + cb = _.noop; + this.emit(channel, { market: this.market, data: message - }); + }, cb); }; }, this) diff --git a/plugins/sqlite/handle.js b/plugins/sqlite/handle.js new file mode 100644 index 000000000..e5c1795c5 --- /dev/null +++ b/plugins/sqlite/handle.js @@ -0,0 +1,55 @@ +var _ = require('lodash'); +var fs = require('fs'); + +var util = require('../../core/util.js'); +var config = util.getConfig(); +var dirs = util.dirs(); + +var adapter = config.adapters.sqlite; + +// verify the correct dependencies are installed +var pluginHelper = require(dirs.core + 'pluginUtil'); +var pluginMock = { + slug: 'sqlite adapter', + dependencies: config.adapters.sqlite.dependencies +}; + +var cannotLoad = pluginHelper.cannotLoad(pluginMock); +if(cannotLoad) + util.die(cannotLoad); + +// should be good now +if(config.debug) + var sqlite3 = require('sqlite3').verbose(); +else + var sqlite3 = require('sqlite3'); + +var plugins = require(util.dirs().gekko + 'plugins'); + +var version = adapter.version; + +var dbName = config.watch.exchange.toLowerCase() + '_' + version + '.db'; +var dir = adapter.dataDirectory; + +var fullPath = [dir, dbName].join('/'); + +var mode = util.gekkoMode(); +if(mode === 'realtime') { + + if(!fs.existsSync(dir)) + fs.mkdirSync(dir); + + +} else if(mode === 'backtest') { + + if(!fs.existsSync(dir)) + util.die('History directory does not exist.'); + + if(!fs.existsSync(fullPath)) + util.die(`History database does not exist for exchange ${config.watch.exchange} at version ${version}.`); +} + + +var db = new sqlite3.Database(fullPath); + +module.exports = db; \ No newline at end of file diff --git a/plugins/sqlite/reader.js b/plugins/sqlite/reader.js new file mode 100644 index 000000000..dec15e653 --- /dev/null +++ b/plugins/sqlite/reader.js @@ -0,0 +1,129 @@ +var _ = require('lodash'); +var util = require('../../core/util.js'); +var config = util.getConfig(); +var log = require(util.dirs().core + 'log'); + +var handle = require('./handle'); +var sqliteUtil = require('./util'); + +var Reader = function() { + _.bindAll(this); + this.db = handle; +} + +// returns the furtherst point (up to `from`) in time we have valid data from +Reader.prototype.mostRecentWindow = function(to, from, next) { + var maxAmount = ((to - from) / 60) + 1; + + this.db.all(` + SELECT start from ${sqliteUtil.table('candles')} + WHERE start <= ${to} AND start >= ${from} + ORDER BY start DESC + `, function(err, rows) { + if(err) { + console.error(err); + return util.die('DB error while reading mostRecentWindow'); + } + + if(rows.length === 0) { + return next(false); + } + + if(rows.length === maxAmount) { + return next(from); + } + + // we have a gap + var gapIndex = _.findIndex(rows, function(r, i) { + return r.start !== to - i * 60; + }); + + // if no candle is recent enough + if(gapIndex === 0) { + return next(false); + } + + // if there was no gap in the records, but + // there were not enough records. + if(gapIndex === -1) + gapIndex = rows.length; + + next(to - gapIndex * 60); + }) +} + +Reader.prototype.get = function(from, to, what, next) { + if(what === 'full') + what = '*'; + + this.db.all(` + SELECT ${what} from ${sqliteUtil.table('candles')} + WHERE start <= ${to} AND start >= ${from} + ORDER BY start ASC + `, function(err, rows) { + if(err) { + console.error(err); + return util.die('DB error at `get`'); + } + + next(null, rows); + }); +} + +Reader.prototype.count = function(from, to, next) { + this.db.all(` + SELECT COUNT(*) as count from ${sqliteUtil.table('candles')} + WHERE start <= ${to} AND start >= ${from} + `, function(err, res) { + if(err) { + console.error(err); + return util.die('DB error at `get`'); + } + + next(null, _.first(res).count); + }); +} + +Reader.prototype.countTotal = function(next) { + this.db.all(` + SELECT COUNT(*) as count from ${sqliteUtil.table('candles')} + `, function(err, res) { + if(err) { + console.error(err); + return util.die('DB error at `get`'); + } + + next(null, _.first(res).count); + }); +} + +Reader.prototype.getBoundry = function(next) { + + this.db.all(` + SELECT + ( + SELECT start + FROM ${sqliteUtil.table('candles')} + ORDER BY start LIMIT 1 + ) as 'first', + ( + SELECT start + FROM ${sqliteUtil.table('candles')} + ORDER BY start DESC + LIMIT 1 + ) as 'last' + `, function(err, rows) { + if(err) { + console.error(err); + return util.die('DB error at `get`'); + } + + next(null, _.first(rows)); + }); +} + +Reader.prototype.close = function() { + this.db = null; +} + +module.exports = Reader; \ No newline at end of file diff --git a/plugins/sqlite/util.js b/plugins/sqlite/util.js new file mode 100644 index 000000000..66d31c9e8 --- /dev/null +++ b/plugins/sqlite/util.js @@ -0,0 +1,15 @@ +var config = require('../../core/util.js').getConfig(); + +var watch = config.watch; +var settings = { + exchange: watch.exchange, + pair: [watch.currency, watch.asset], + historyPath: config.adapters.sqlite.dataDirectory +} + +module.exports = { + settings: settings, + table: function(name) { + return [name, settings.pair.join('_')].join('_'); + } +} \ No newline at end of file diff --git a/plugins/sqlite/writer.js b/plugins/sqlite/writer.js new file mode 100644 index 000000000..19b3e7e23 --- /dev/null +++ b/plugins/sqlite/writer.js @@ -0,0 +1,109 @@ +var _ = require('lodash'); +var config = require('../../core/util.js').getConfig(); + +var handle = require('./handle'); +var sqliteUtil = require('./util'); + +var Store = function(done, pluginMeta) { + _.bindAll(this); + this.done = done; + + this.db = handle; + this.db.serialize(this.upsertTables); + + this.cache = []; +} + +Store.prototype.upsertTables = function() { + var createQueries = [ + ` + CREATE TABLE IF NOT EXISTS + ${sqliteUtil.table('candles')} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + start INTEGER UNIQUE, + open REAL NOT NULL, + high REAL NOT NULL, + low REAL NOT NULL, + close REAL NOT NULL, + vwp REAL NOT NULL, + volume REAL NOT NULL, + trades INTERGER NOT NULL + ); + `, + + // TODO: create trades + // `` + + // TODO: create advices + // `` + ]; + + var next = _.after(_.size(createQueries), this.done); + + _.each(createQueries, function(q) { + this.db.run(q, next); + }, this); +} + +Store.prototype.writeCandles = function() { + if(_.isEmpty(this.cache)) + return; + + var stmt = this.db.prepare(` + INSERT OR IGNORE INTO ${sqliteUtil.table('candles')} + VALUES (?,?,?,?,?,?,?,?,?) + `); + + _.each(this.cache, candle => { + stmt.run( + null, + candle.start.unix(), + candle.open, + candle.high, + candle.low, + candle.close, + candle.vwp, + candle.volume, + candle.trades + ); + }); + + stmt.finalize(); + + this.cache = []; +} + +var processCandle = function(candle, done) { + + // because we might get a lot of candles + // in the same tick, we rather batch them + // up and insert them at once at next tick. + this.cache.push(candle); + _.defer(this.writeCandles); + + // NOTE: sqlite3 has it's own buffering, at + // this point we are confident that the candle will + // get written to disk on next tick. + done(); +} + +if(config.candleWriter.enabled) + Store.prototype.processCandle = processCandle; + +// TODO: add storing of trades / advice? + +// var processTrades = function(candles) { +// util.die('NOT IMPLEMENTED'); +// } + +// var processAdvice = function(candles) { +// util.die('NOT IMPLEMENTED'); +// } + +// if(config.tradeWriter.enabled) +// Store.prototype.processTrades = processTrades; + +// if(config.adviceWriter.enabled) +// Store.prototype.processAdvice = processAdvice; + +module.exports = Store; \ No newline at end of file diff --git a/plugins/webserver.js b/plugins/webserver.js index d79ee7ed1..c5cb8b283 100644 --- a/plugins/webserver.js +++ b/plugins/webserver.js @@ -10,16 +10,14 @@ var Actor = function(next) { this.server.setup(next); } -Actor.prototype.processTrade = function(trade) { - this.server.broadcastTrade(trade); -}; - Actor.prototype.init = function(data) { this.server.broadcastHistory(data); }; -Actor.prototype.processSmallCandle = function(candle) { - this.server.broadcastSmallCandle(candle); +Actor.prototype.processCandle = function(candle, next) { + this.server.broadcastCandle(candle); + + next(); }; Actor.prototype.processAdvice = function(advice) { diff --git a/plugins/xmppbot.js b/plugins/xmppbot.js new file mode 100644 index 000000000..86eaf4076 --- /dev/null +++ b/plugins/xmppbot.js @@ -0,0 +1,190 @@ +var log = require('../core/log'); +var moment = require('moment'); +var _ = require('lodash'); +var xmpp = require('node-xmpp'); +var config = require('../core/util').getConfig(); +var xmppbot = config.xmppbot; +var utc = moment.utc; + + + +var Actor = function() { + _.bindAll(this); + + this.bot = new xmpp.Client({ jid: xmppbot.client_id, + password: xmppbot.client_pwd, + host: xmppbot.client_host, + port: xmppbot.client_port + }); + + this.advice = 'Dont got one yet :('; + this.adviceTime = utc(); + this.state = xmppbot.status_msg; + this.price = 'Dont know yet :('; + this.priceTime = utc(); + + this.commands = { + ';;advice': 'emitAdvice', + ';;price': 'emitPrice', + ';;donate': 'emitDonation', + ';;real advice': 'emitRealAdvice', + ';;help': 'emitHelp' + }; + + this.rawCommands = _.keys(this.commands); + + this.bot.addListener('online', this.setState); + this.bot.addListener('stanza', this.rawStanza); + this.bot.addListener("error", this.logError); + +} + +Actor.prototype.setState = function() { + var elem = new xmpp.Element('presence', { }).c('show').t('chat').up().c('status').t(this.state) + this.bot.send(elem); +}; + +Actor.prototype.rawStanza = function(stanza) { + if (stanza.is('presence') && (stanza.attrs.type == 'subscribe')) { + this.bot.send(new xmpp.Element('presence', { to: stanza.attrs.from, type: 'subscribed' })); + } + if (stanza.is('message') && + // Important: never reply to errors! + stanza.attrs.type !== 'error') { + + // Swap addresses... + var from = stanza.attrs.from; + var body = stanza.getChild('body'); + if (!body) { + return; + } + + var message_recv = body.getText(); //Get Incoming Message + this.verifyQuestion(from, message_recv); + } +}; + +Actor.prototype.sendMessageTo = function(receiver, message){ + this.bot.send(new xmpp.Element('message', { to: receiver, type: 'chat' }). + c('body').t(message) + ); +}; +Actor.prototype.sendMessage = function(message) { + this.sendMessageTo(this.from, message); +}; + +Actor.prototype.processCandle = function(candle) { + this.price = candle.close; + this.priceTime = candle.date; +}; + +Actor.prototype.processAdvice = function(advice) { + this.advice = advice.recommandation; + this.adviceTime = utc(); + + if(xmppbot.emitUpdats) + this.newAdvice(xmppbot.receiver); +}; + +Actor.prototype.verifyQuestion = function(receiver, text) { + if(text in this.commands) + this[this.commands[text]](receiver); +} + +Actor.prototype.newAdvice = function(receiver) { + this.sendMessageTo(receiver, 'Important news!'); + this.emitAdvice(receiver); +} + +// sent advice +Actor.prototype.emitAdvice = function(receiver) { + var message = [ + 'Advice for ', + config.watch.exchange, + ' ', + config.watch.currency, + '/', + config.watch.asset, + ' using ', + config.tradingAdvisor.method, + ' at ', + config.tradingAdvisor.candleSize, + ' minute candles, is:\n', + this.advice, + ' ', + config.watch.asset, + ' (from ', + this.adviceTime.fromNow(), + ')' + ].join(''); + + this.sendMessageTo(receiver, message); +}; + +// sent price +Actor.prototype.emitPrice = function(receiver) { + + var message = [ + 'Current price at ', + config.watch.exchange, + ' ', + config.watch.currency, + '/', + config.watch.asset, + ' is ', + this.price, + ' ', + config.watch.currency, + ' (from ', + this.priceTime.fromNow(), + ')' + ].join(''); + + this.sendMessageTo(receiver, message); +}; + +// sent donation info +Actor.prototype.emitDonation = function(receiver) { + var message = 'You want to donate? How nice of you! You can send your coins here:'; + message += '\nBTC:\t19UGvmFPfFyFhPMHu61HTMGJqXRdVcHAj3'; + + this.sendMessageTo(receiver, message); +}; + +Actor.prototype.emitHelp = function(receiver) { + var message = _.reduce( + this.rawCommands, + function(message, command) { + return message + ' ' + command + ','; + }, + 'possible commands are:' + ); + + message = message.substr(0, _.size(message) - 1) + '.'; + + this.sendMessageTo(receiver, message); + +} + +Actor.prototype.emitRealAdvice = function(receiver) { + // http://www.examiner.com/article/uncaged-a-look-at-the-top-10-quotes-of-gordon-gekko + // http://elitedaily.com/money/memorable-gordon-gekko-quotes/ + var realAdvice = [ + 'I don\'t throw darts at a board. I bet on sure things. Read Sun-tzu, The Art of War. Every battle is won before it is ever fought.', + 'Ever wonder why fund managers can\'t beat the S&P 500? \'Cause they\'re sheep, and sheep get slaughtered.', + 'If you\'re not inside, you\'re outside!', + 'The most valuable commodity I know of is information.', + 'It\'s not a question of enough, pal. It\'s a zero sum game, somebody wins, somebody loses. Money itself isn\'t lost or made, it\'s simply transferred from one perception to another.', + 'What’s worth doing is worth doing for money. (Wait, wasn\'t I a free and open source bot?)', + 'When I get a hold of the son of a bitch who leaked this, I’m gonna tear his eyeballs out and I’m gonna suck his fucking skull.' + ]; + + this.sendMessageTo(receiver, _.first(_.shuffle(realAdvice))); +} + +Actor.prototype.logError = function(message) { + log.error('XMPP ERROR:', message); +}; + + +module.exports = Actor; diff --git a/subscriptions.js b/subscriptions.js index 934fefacb..742a6ce1e 100644 --- a/subscriptions.js +++ b/subscriptions.js @@ -1,9 +1,6 @@ // -// Subscriptions glue actors to events -// flowing through the Gekko. This -// specifies how actors can be glued -// to events and where those events -// are emitted from (internally). +// Subscriptions glue plugins to events +// flowing through the Gekko. // var subscriptions = [ @@ -28,7 +25,7 @@ var subscriptions = [ handler: 'processHistory' }, { - emitter: 'advisor', + emitter: 'tradingAdvisor', event: 'advice', handler: 'processAdvice' } diff --git a/test/_prepare.js b/test/_prepare.js new file mode 100644 index 000000000..3ca109d8f --- /dev/null +++ b/test/_prepare.js @@ -0,0 +1,4 @@ +var utils = require(__dirname + '/../core/util'); +var config = utils.getConfig(); +config.debug = false; +utils.setConfig(config); \ No newline at end of file diff --git a/test/candleBatcher.js b/test/candleBatcher.js new file mode 100644 index 000000000..469e239e8 --- /dev/null +++ b/test/candleBatcher.js @@ -0,0 +1,93 @@ +var chai = require('chai'); +var expect = chai.expect; +var should = chai.should; +var sinon = require('sinon'); + +var _ = require('lodash'); +var moment = require('moment'); + +var utils = require(__dirname + '/../core/util'); + +var dirs = utils.dirs(); +var CandleBatcher = require(dirs.core + 'candleBatcher'); + +var candles = [ + {"start":moment("2015-02-14T23:57:00.000Z"),"open":257.19,"high":257.19,"low":257.18,"close":257.18,"vwp":257.18559990418294,"volume":0.97206065,"trades":2}, + {"start":moment("2015-02-14T23:58:00.000Z"),"open":257.02,"high":257.02,"low":256.98,"close":256.98,"vwp":257.0175849772836,"volume":4.1407478,"trades":2}, + {"start":moment("2015-02-14T23:59:00.000Z"),"open":256.85,"high":256.99,"low":256.85,"close":256.99,"vwp":256.9376998467,"volume":6,"trades":6}, + {"start":moment("2015-02-15T00:00:00.000Z"),"open":256.81,"high":256.82,"low":256.81,"close":256.82,"vwp":256.815,"volume":4,"trades":2}, + {"start":moment("2015-02-15T00:01:00.000Z"),"open":256.81,"high":257.02,"low":256.81,"close":257.01,"vwp":256.94666666666666,"volume":6,"trades":3}, + {"start":moment("2015-02-15T00:02:00.000Z"),"open":257.03,"high":257.03,"low":256.33,"close":256.33,"vwp":256.74257263558013,"volume":6.7551178,"trades":6}, + {"start":moment("2015-02-15T00:03:00.000Z"),"open":257.02,"high":257.47,"low":257.02,"close":257.47,"vwp":257.26466004728906,"volume":3.7384995300000003,"trades":3}, + {"start":moment("2015-02-15T00:04:00.000Z"),"open":257.47,"high":257.48,"low":257.37,"close":257.38,"vwp":257.4277429116875,"volume":8,"trades":6}, + {"start":moment("2015-02-15T00:05:00.000Z"),"open":257.38,"high":257.45,"low":257.38,"close":257.45,"vwp":257.3975644932184,"volume":7.97062564,"trades":4}, + {"start":moment("2015-02-15T00:06:00.000Z"),"open":257.46,"high":257.48,"low":257.46,"close":257.48,"vwp":257.47333333333336,"volume":7.5,"trades":4} +]; + +describe('candleBatcher', function() { + var cb; + + it('should throw when not passed a number', function() { + expect(function() { + new CandleBatcher(); + }).to.throw('candleSize is not a number'); + }); + + it('should instantiate', function() { + cb = new CandleBatcher(2); + }); + + it('should throw when fed a candle', function() { + var candle = _.first(candles); + expect( + cb.write.bind(cb, candle) + ).to.throw('candles is not an array'); + }); + + it('should not emit an event when fed not enough candles', function() { + var candle = _.first(candles); + + var spy = sinon.spy(); + cb.on('candle', spy); + cb.write( [candle] ); + expect(spy.called).to.be.false; + }); + + it('should emit 5 events when fed 10 candles', function() { + cb = new CandleBatcher(2); + + var spy = sinon.spy(); + cb.on('candle', spy); + cb.write( candles ); + expect(spy.callCount).to.equal(5); + }); + + it('should correctly add two candles together', function() { + cb = new CandleBatcher(2); + var _candles = _.first(candles, 2); + var first = _.first(_candles); + var second = _.last(_candles); + + var result = { + start: first.start, + open: first.open, + high: _.max([first.high, second.high]), + low: _.min([first.low, second.low]), + close: second.close, + volume: first.volume + second.volume, + vwp: (first.vwp * first.volume) + (second.vwp * second.volume), + trades: first.trades + second.trades + }; + + result.vwp /= result.volume; + + var spy = sinon.spy(); + cb.on('candle', spy); + cb.write( _candles ); + + var cbResult = _.first(_.first(spy.args)); + expect(cbResult).to.deep.equal(result); + + }); + +}); \ No newline at end of file diff --git a/test/candleStore.js b/test/candleStore.js index 7e979abd5..63e874cd3 100644 --- a/test/candleStore.js +++ b/test/candleStore.js @@ -1,247 +1,249 @@ -var fs = require('fs'); -var zlib = require('zlib'); -var async = require('async'); -var _ = require('lodash'); -var CSVStore = require('../core/candleStore.js'); +// TODO: rewrite tests for mocha once we use Days. -var TMPDIR = "./tmp/"; -var CSVNAME = "test.csv"; -var CSVGEN = "gen.csv"; -var DAY_FILE = "day.csv"; -var CSVFILE = TMPDIR + CSVNAME; -var CSVGENFILE = TMPDIR + CSVGEN; -var DAY_PATH = TMPDIR + DAY_FILE; -var LOAD_DAY = "2009-01-12"; -var LOAD_FILE = "history-"+LOAD_DAY+".csv"; -var LOAD_PATH = TMPDIR + LOAD_FILE; -var CANDLES = [{ - s: 1, - o: 2, - h: 3, - l: 4, - c: 5, - p: 6 -}, { - s: 10, - o: 20, - h: 30, - l: 40, - c: 50, - p: 60 -}]; -var DATA = "1,2,3,4,5,6\n" + "10,20,30,40,50,60"; -var GEN_CANDLES = 1440; -var CANDLES_DAY = _.map(_.range(GEN_CANDLES), function (d) { - //TODO(yin): Randomness in test is bad, figure out a static seed - var rnd = function () { - return Math.random() * 1000 - } - return { - s: d, - o: rnd().toString(), - h: rnd().toString(), - l: rnd().toString(), - c: rnd().toString(), - p: rnd().toString() - }; -}); +// var fs = require('fs'); +// var zlib = require('zlib'); +// var async = require('async'); +// var _ = require('lodash'); +// var CSVStore = require('../core/candleStore.js'); -var timer; -var csv; +// var TMPDIR = "./tmp/"; +// var CSVNAME = "test.csv"; +// var CSVGEN = "gen.csv"; +// var DAY_FILE = "day.csv"; +// var CSVFILE = TMPDIR + CSVNAME; +// var CSVGENFILE = TMPDIR + CSVGEN; +// var DAY_PATH = TMPDIR + DAY_FILE; +// var LOAD_DAY = "2009-01-12"; +// var LOAD_FILE = "history-"+LOAD_DAY+".csv"; +// var LOAD_PATH = TMPDIR + LOAD_FILE; +// var CANDLES = [{ +// s: 1, +// o: 2, +// h: 3, +// l: 4, +// c: 5, +// p: 6 +// }, { +// s: 10, +// o: 20, +// h: 30, +// l: 40, +// c: 50, +// p: 60 +// }]; +// var DATA = "1,2,3,4,5,6\n" + "10,20,30,40,50,60"; +// var GEN_CANDLES = 1440; +// var CANDLES_DAY = _.map(_.range(GEN_CANDLES), function (d) { +// //TODO(yin): Randomness in test is bad, figure out a static seed +// var rnd = function () { +// return Math.random() * 1000 +// } +// return { +// s: d, +// o: rnd().toString(), +// h: rnd().toString(), +// l: rnd().toString(), +// c: rnd().toString(), +// p: rnd().toString() +// }; +// }); -function cleanUp(path, next) { - if (fs.existsSync(path)) { - var files = []; - files = fs.readdirSync(path); - files.forEach(function (file, index) { - var curPath = path + "/" + file; - if (fs.statSync(curPath).isDirectory()) { - // recurse - cleanUp(curPath); - } else { - // delete file - fs.unlinkSync(curPath); - } - }); - fs.rmdirSync(path); - } - next(null, path); -} +// var timer; +// var csv; -function buffersEqual(a, b) { - if (!Buffer.isBuffer(a)) return undefined; - if (!Buffer.isBuffer(b)) return undefined; - if (a.length !== b.length) return false; - for (var i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; -}; +// function cleanUp(path, next) { +// if (fs.existsSync(path)) { +// var files = []; +// files = fs.readdirSync(path); +// files.forEach(function (file, index) { +// var curPath = path + "/" + file; +// if (fs.statSync(curPath).isDirectory()) { +// // recurse +// cleanUp(curPath); +// } else { +// // delete file +// fs.unlinkSync(curPath); +// } +// }); +// fs.rmdirSync(path); +// } +// next(null, path); +// } -var Timer = function () { - this.precision = 3; - this.checkpoints = []; - this.time_now = function (note) { - var elapsed; - var cp_count = this.checkpoints.length; +// function buffersEqual(a, b) { +// if (!Buffer.isBuffer(a)) return undefined; +// if (!Buffer.isBuffer(b)) return undefined; +// if (a.length !== b.length) return false; +// for (var i = 0; i < a.length; i++) { +// if (a[i] !== b[i]) return false; +// } +// return true; +// }; - if (cp_count > 0) { - var last = this.checkpoints[cp_count - 1]; - var diff = process.hrtime(last); - elapsed = diff[0] * 1000 + diff[1] / 100000; - console.log("Elapsed: " + elapsed.toFixed(this.precision) + " ms - " + note); - } - this.checkpoints.push(process.hrtime()); - } -} +// var Timer = function () { +// this.precision = 3; +// this.checkpoints = []; +// this.time_now = function (note) { +// var elapsed; +// var cp_count = this.checkpoints.length; -function buffersEqual(a, b) { - if (!Buffer.isBuffer(a)) return undefined; - if (!Buffer.isBuffer(b)) return undefined; - if (a.length !== b.length) return false; - for (var i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; -}; +// if (cp_count > 0) { +// var last = this.checkpoints[cp_count - 1]; +// var diff = process.hrtime(last); +// elapsed = diff[0] * 1000 + diff[1] / 100000; +// console.log("Elapsed: " + elapsed.toFixed(this.precision) + " ms - " + note); +// } +// this.checkpoints.push(process.hrtime()); +// } +// } -var Timer = function () { - this.precision = 3; - this.checkpoints = []; - this.time_now = function (note) { - var elapsed; - var cp_count = this.checkpoints.length; +// function buffersEqual(a, b) { +// if (!Buffer.isBuffer(a)) return undefined; +// if (!Buffer.isBuffer(b)) return undefined; +// if (a.length !== b.length) return false; +// for (var i = 0; i < a.length; i++) { +// if (a[i] !== b[i]) return false; +// } +// return true; +// }; - if (cp_count > 0) { - var last = this.checkpoints[cp_count - 1]; - var diff = process.hrtime(last); - elapsed = diff[0] * 1000 + diff[1] / 100000; - console.log("Elapsed: " + elapsed.toFixed(this.precision) + " ms - " + note); - } - this.checkpoints.push(process.hrtime()); - } -} +// var Timer = function () { +// this.precision = 3; +// this.checkpoints = []; +// this.time_now = function (note) { +// var elapsed; +// var cp_count = this.checkpoints.length; -//TODO(yin): tearDownCandleFile() at end of tests -var setupCandleFile = function(filename, data, done) { - fs.existsSync(TMPDIR) || fs.mkdirSync(TMPDIR); - zlib.deflate(data, function(err, buffer) { - fs.writeFile(filename, buffer, function(err) { - done(err); - }); - }); -} +// if (cp_count > 0) { +// var last = this.checkpoints[cp_count - 1]; +// var diff = process.hrtime(last); +// elapsed = diff[0] * 1000 + diff[1] / 100000; +// console.log("Elapsed: " + elapsed.toFixed(this.precision) + " ms - " + note); +// } +// this.checkpoints.push(process.hrtime()); +// } +// } -module.exports = { - // runs before each test method invocation - setUp: function (done) { - timer = new Timer(); - csv = new CSVStore(); - csv.directory = TMPDIR; - done(); - }, - // runs after each test method invocation - tearDown: function (done) { -// fs.existsSync(TMPDIR) && cleanUp(TMPDIR, done); - timer = null; - done(); - }, - test_loadFile: function (test) { - setupCandleFile(CSVFILE, DATA, function() { - timer.time_now(); - try { - csv.read(CSVNAME, function (candles) { - timer.time_now('candles parsed'); - // we must catch failed asserts and print ourself - try { - test.deepEqual(candles, CANDLES, "Loaded candles seem corrupt: " + candles); - test.done(); - } catch (err) { - console.log(err); - throw err; - } - }); - } catch (err) { - console.log(err); - throw err; - } - }); - }, - test_saveFile: function (test) { - timer.time_now(); - //TODO(yin):Mock the fs. - csv.write(CSVGEN, CANDLES, function (err) { - timer.time_now("candles written"); - fs.readFile(CSVGENFILE, function (err, buffer) { - if (err) { - console.log(err); - throw err; - } - zlib.deflate(DATA, function (err, databuf) { - // ensure assertion's are printed - try { - test.ok(buffersEqual(buffer, databuf), "Persisted candles seem corrupt"); - test.done(); - } catch (err) { - console.log(err); - throw err; - } - }); - }); - }); - }, - test_saveLoadDay: function (test) { - timer.time_now(); - //TODO(yin):Mock the fs. - csv.write(DAY_FILE, CANDLES_DAY, function (candles) { - timer.time_now("candles written"); - csv.read(DAY_FILE, function (candles) { - timer.time_now('candles parsed'); - // we must catch failed asserts and print ourself - try { - test.deepEqual(candles, CANDLES_DAY); - test.done(); - } catch (err) { - //TODO(yin):too much output be generated if this is printed - throw err; - } - }); - }); - }, - test_candlesAreNumbers: function (test) { - setupCandleFile(CSVNAME, DATA, function() { - timer.time_now(); - csv.read(CSVNAME, function (err, candles) { - timer.time_now('candles parsed'); - try { - _.each(candles, function (c) { - console.log(c); - //TODO(yin): This generates also too much output - test.equals(typeof (c.s), 'number'); - test.equals(typeof (c.o), 'number'); - test.equals(typeof (c.h), 'number'); - test.equals(typeof (c.l), 'number'); - test.equals(typeof (c.p), 'number'); - }); - test.done(); - } catch (err) { - throw err; - } - }); - }); - }, - test_loadDay: function(test) { - setupCandleFile(LOAD_PATH, DATA, function() { - csv.loadDay(LOAD_DAY, function(err, candles) { -console.log(10); - try { - test.equal(err, null, "No error should happen"); - test.deepEqual(candles, CANDLES, "Store should load candles in from a day"); - test.done(); - } catch(err) { - console.log(err); - } - }); - }); - } -}; +// //TODO(yin): tearDownCandleFile() at end of tests +// var setupCandleFile = function(filename, data, done) { +// fs.existsSync(TMPDIR) || fs.mkdirSync(TMPDIR); +// zlib.deflate(data, function(err, buffer) { +// fs.writeFile(filename, buffer, function(err) { +// done(err); +// }); +// }); +// } + +// module.exports = { +// // runs before each test method invocation +// setUp: function (done) { +// timer = new Timer(); +// csv = new CSVStore(); +// csv.directory = TMPDIR; +// done(); +// }, +// // runs after each test method invocation +// tearDown: function (done) { +// // fs.existsSync(TMPDIR) && cleanUp(TMPDIR, done); +// timer = null; +// done(); +// }, +// test_loadFile: function (test) { +// setupCandleFile(CSVFILE, DATA, function() { +// timer.time_now(); +// try { +// csv.read(CSVNAME, function (candles) { +// timer.time_now('candles parsed'); +// // we must catch failed asserts and print ourself +// try { +// test.deepEqual(candles, CANDLES, "Loaded candles seem corrupt: " + candles); +// test.done(); +// } catch (err) { +// console.log(err); +// throw err; +// } +// }); +// } catch (err) { +// console.log(err); +// throw err; +// } +// }); +// }, +// test_saveFile: function (test) { +// timer.time_now(); +// //TODO(yin):Mock the fs. +// csv.write(CSVGEN, CANDLES, function (err) { +// timer.time_now("candles written"); +// fs.readFile(CSVGENFILE, function (err, buffer) { +// if (err) { +// console.log(err); +// throw err; +// } +// zlib.deflate(DATA, function (err, databuf) { +// // ensure assertion's are printed +// try { +// test.ok(buffersEqual(buffer, databuf), "Persisted candles seem corrupt"); +// test.done(); +// } catch (err) { +// console.log(err); +// throw err; +// } +// }); +// }); +// }); +// }, +// test_saveLoadDay: function (test) { +// timer.time_now(); +// //TODO(yin):Mock the fs. +// csv.write(DAY_FILE, CANDLES_DAY, function (candles) { +// timer.time_now("candles written"); +// csv.read(DAY_FILE, function (candles) { +// timer.time_now('candles parsed'); +// // we must catch failed asserts and print ourself +// try { +// test.deepEqual(candles, CANDLES_DAY); +// test.done(); +// } catch (err) { +// //TODO(yin):too much output be generated if this is printed +// throw err; +// } +// }); +// }); +// }, +// test_candlesAreNumbers: function (test) { +// setupCandleFile(CSVNAME, DATA, function() { +// timer.time_now(); +// csv.read(CSVNAME, function (err, candles) { +// timer.time_now('candles parsed'); +// try { +// _.each(candles, function (c) { +// console.log(c); +// //TODO(yin): This generates also too much output +// test.equals(typeof (c.s), 'number'); +// test.equals(typeof (c.o), 'number'); +// test.equals(typeof (c.h), 'number'); +// test.equals(typeof (c.l), 'number'); +// test.equals(typeof (c.p), 'number'); +// }); +// test.done(); +// } catch (err) { +// throw err; +// } +// }); +// }); +// }, +// test_loadDay: function(test) { +// setupCandleFile(LOAD_PATH, DATA, function() { +// csv.loadDay(LOAD_DAY, function(err, candles) { +// console.log(10); +// try { +// test.equal(err, null, "No error should happen"); +// test.deepEqual(candles, CANDLES, "Store should load candles in from a day"); +// test.done(); +// } catch(err) { +// console.log(err); +// } +// }); +// }); +// } +// }; diff --git a/test/day.js b/test/day.js index d21de1b57..8ee7178fd 100644 --- a/test/day.js +++ b/test/day.js @@ -1,47 +1,49 @@ -var Day = require('../core/candleStore.js').Day; +// TODO: rewrite tests for mocha once we use Days. -module.exports = { - //TODO(yin): Discuss test naming conventions with outhers. - day_assumptions: function(test) { - var day = new Day("my-birthsday"); - // NOTE(yin):Yes, usualy you do test for every elementary assumption possible, if - // not tested by a smaller test. - // This helps pinpoint the cause faster and aughts to give the developer a meaninful message - // for each assertion test ('should' is the keyword in assertion messages). - test.equals(day.candles.length, 0, "Day should be initialized empty (no candles).") - test.equals(day.state, 'uninitialized', "Day should be initialized in state 'uninitialized'.") - test.done(); - }, - day_addCandles: function(test) { - var day = new Day("my-birthsday"); - var candles = [ - { s: 1 }, - { s: 2 }, - { s: 3 } - ]; - day.addCandles(candles); - test.deepEqual(day.candles, candles, "Day should contain candles previously appended to it."); - test.done(); - }, - //NOTE(yin): Test every possibility. If smaller test pass, regrression in this test may - // may indicate, i.e. addCandles() replaces, and not concatenates candles. - day_addCandles2: function(test) { - var day = new Day("my-birthsday"); - var candles = [ - { s: 1 }, - { s: 2 }, - { s: 3 } - ]; - var candles2 = [ - { s: 10 }, - ]; - var candlesExpect = candles.concat(candles2); - day.addCandles(candles); - day.addCandles(candles2); - test.equals(day.candles.length, candles.length + candles2.length, - "Day should contain exactly as much candles as we added to it."); - test.deepEqual(day.candles, candlesExpect, - "Last candles in to a Day should be the last appended to it.") - test.done(); - }, -}; +// var Day = require('../core/candleStore.js').Day; + +// module.exports = { +// //TODO(yin): Discuss test naming conventions with outhers. +// day_assumptions: function(test) { +// var day = new Day("my-birthsday"); +// // NOTE(yin):Yes, usualy you do test for every elementary assumption possible, if +// // not tested by a smaller test. +// // This helps pinpoint the cause faster and aughts to give the developer a meaninful message +// // for each assertion test ('should' is the keyword in assertion messages). +// test.equals(day.candles.length, 0, "Day should be initialized empty (no candles).") +// test.equals(day.state, 'uninitialized', "Day should be initialized in state 'uninitialized'.") +// test.done(); +// }, +// day_addCandles: function(test) { +// var day = new Day("my-birthsday"); +// var candles = [ +// { s: 1 }, +// { s: 2 }, +// { s: 3 } +// ]; +// day.addCandles(candles); +// test.deepEqual(day.candles, candles, "Day should contain candles previously appended to it."); +// test.done(); +// }, +// //NOTE(yin): Test every possibility. If smaller test pass, regrression in this test may +// // may indicate, i.e. addCandles() replaces, and not concatenates candles. +// day_addCandles2: function(test) { +// var day = new Day("my-birthsday"); +// var candles = [ +// { s: 1 }, +// { s: 2 }, +// { s: 3 } +// ]; +// var candles2 = [ +// { s: 10 }, +// ]; +// var candlesExpect = candles.concat(candles2); +// day.addCandles(candles); +// day.addCandles(candles2); +// test.equals(day.candles.length, candles.length + candles2.length, +// "Day should contain exactly as much candles as we added to it."); +// test.deepEqual(day.candles, candlesExpect, +// "Last candles in to a Day should be the last appended to it.") +// test.done(); +// }, +// }; diff --git a/test/exchangeTests.js b/test/exchangeTests.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/indicators.js b/test/indicators.js index 2bbe439d4..6e271d7da 100644 --- a/test/indicators.js +++ b/test/indicators.js @@ -1,11 +1,9 @@ -var _ = require('lodash'); - -var EMA = require('../methods/indicators/EMA.js'); -var DEMA = require('../methods/indicators/DEMA.js'); -var MACD = require('../methods/indicators/MACD.js'); -var PPO = require('../methods/indicators/PPO.js'); +var chai = require('chai'); +var expect = chai.expect; +var should = chai.should; +var sinon = require('sinon'); -var test = {}; +var _ = require('lodash'); // Fake input prices to verify all indicators // are working correctly by comparing fresh @@ -18,103 +16,120 @@ var test = {}; var prices = [81, 24, 75, 21, 34, 25, 72, 92, 99, 2, 86, 80, 76, 8, 87, 75, 32, 65, 41, 9, 13, 26, 56, 28, 65, 58, 17, 90, 87, 86, 99, 3, 70, 1, 27, 9, 92, 68, 9]; -// EMA results +describe('Indicators', function() { + + describe('EMA', function() { + + var EMA = require('../methods/indicators/EMA.js'); -var verified_ema10results = [81,70.63636363636363,71.4297520661157,62.26070623591284,57.12239601120141,51.28196037280115,55.04887666865549,61.767262728899944,68.53685132364541,56.43924199207351,61.81392526624197,65.12048430874341,67.09857807079005,56.35338205791913,61.92549441102474,64.30267724538388,58.42946320076862,59.624106255174325,56.2379051178699,47.649195096439,41.349341442541004,38.55855208935173,41.72972443674232,39.233410902789174,43.91824528410023,46.47856432335473,41.118825355472055,50.00631165447713,56.73243680820856,62.053811933988825,68.77130067326358,56.81288236903384,59.21054012011859,48.626805552824294,44.69465908867441,38.204721072551784,47.985680877542364,51.62464799071648,43.874711992404386]; -var verified_ema12results = [81,72.23076923076923,72.65680473372781,64.709604005462,59.98504954308323,54.602734228762735,57.27923665510693,62.620892554321244,68.2176783151949,58.030343189780304,62.33336731442949,65.05131080451726,66.73572452689922,57.69945921506857,62.20723472044264,64.17535245575915,59.2252982317962,60.11371388844294,57.173142520990176,49.761889825453224,44.10621446769119,41.320643011123316,43.57900562479665,41.182235528674084,44.84650698580115,46.87012129567789,42.27471801941975,49.61706909335518,55.368289232839,60.080860120094535,66.06842010161846,56.365586239831,58.46318835678008,49.62269784035237,46.14228278799047,40.42808543599194,48.36222613814702,51.383422116893634,44.86289563737154]; -var verified_ema26results = [81,76.77777777777777,76.64609053497942,72.52415790275873,69.67051657662846,66.36158942280413,66.77924946555937,68.64745320885127,70.89579000819562,65.79239815573669,67.28925755160805,68.2307940292667,68.80629076783954,64.30212108133291,65.98344544567863,66.65133837562836,64.08457257002625,64.15238200928357,62.43739074933664,58.479065508645036,55.11024584133799,52.95393133457221,53.17956605052982,51.314413009749835,52.32816019421281,52.748296476122974,50.10027451492868,53.05580973604507,55.57019420004173,57.82425388892753,60.874309156414384,56.58732329297628,57.58085490090396,53.38968046379996,51.4348893183333,48.29156418364194,51.52922609596476,52.74928342218959,49.50859576128666]; + var verified_ema10results = [81,70.63636363636363,71.4297520661157,62.26070623591284,57.12239601120141,51.28196037280115,55.04887666865549,61.767262728899944,68.53685132364541,56.43924199207351,61.81392526624197,65.12048430874341,67.09857807079005,56.35338205791913,61.92549441102474,64.30267724538388,58.42946320076862,59.624106255174325,56.2379051178699,47.649195096439,41.349341442541004,38.55855208935173,41.72972443674232,39.233410902789174,43.91824528410023,46.47856432335473,41.118825355472055,50.00631165447713,56.73243680820856,62.053811933988825,68.77130067326358,56.81288236903384,59.21054012011859,48.626805552824294,44.69465908867441,38.204721072551784,47.985680877542364,51.62464799071648,43.874711992404386]; + var verified_ema12results = [81,72.23076923076923,72.65680473372781,64.709604005462,59.98504954308323,54.602734228762735,57.27923665510693,62.620892554321244,68.2176783151949,58.030343189780304,62.33336731442949,65.05131080451726,66.73572452689922,57.69945921506857,62.20723472044264,64.17535245575915,59.2252982317962,60.11371388844294,57.173142520990176,49.761889825453224,44.10621446769119,41.320643011123316,43.57900562479665,41.182235528674084,44.84650698580115,46.87012129567789,42.27471801941975,49.61706909335518,55.368289232839,60.080860120094535,66.06842010161846,56.365586239831,58.46318835678008,49.62269784035237,46.14228278799047,40.42808543599194,48.36222613814702,51.383422116893634,44.86289563737154]; + var verified_ema26results = [81,76.77777777777777,76.64609053497942,72.52415790275873,69.67051657662846,66.36158942280413,66.77924946555937,68.64745320885127,70.89579000819562,65.79239815573669,67.28925755160805,68.2307940292667,68.80629076783954,64.30212108133291,65.98344544567863,66.65133837562836,64.08457257002625,64.15238200928357,62.43739074933664,58.479065508645036,55.11024584133799,52.95393133457221,53.17956605052982,51.314413009749835,52.32816019421281,52.748296476122974,50.10027451492868,53.05580973604507,55.57019420004173,57.82425388892753,60.874309156414384,56.58732329297628,57.58085490090396,53.38968046379996,51.4348893183333,48.29156418364194,51.52922609596476,52.74928342218959,49.50859576128666]; -test.ema = { - _10: function(test) { - var ema = new EMA(10); - _.each(prices, function(p, i) { - ema.update(p); - test.equals(ema.result, verified_ema10results[i]); + it('should correctly calculate EMAs with weight 10', function() { + var ema = new EMA(10); + _.each(prices, function(p, i) { + ema.update(p); + expect(ema.result).to.equal(verified_ema10results[i]); + }); }); - test.done(); - }, - _12: function(test) { - var ema = new EMA(12); - _.each(prices, function(p, i) { - ema.update(p); - test.equals(ema.result, verified_ema12results[i]); + + it('should correctly calculate EMAs with weight 12', function() { + var ema = new EMA(12); + _.each(prices, function(p, i) { + ema.update(p); + expect(ema.result).to.equal(verified_ema12results[i]); + }); + }); + + it('should correctly calculate EMAs with weight 26', function() { + var ema = new EMA(26); + _.each(prices, function(p, i) { + ema.update(p); + expect(ema.result).to.equal(verified_ema26results[i]); + }); }); - test.done(); - }, - _26: function(test) { - var ema = new EMA(26); - _.each(prices, function(p, i) { - ema.update(p); - test.equals(ema.result, verified_ema26results[i]); + + }); + + + describe('MACD', function() { + + var MACD = require('../methods/indicators/MACD.js'); + + var verified_macd12v26v9diff = [0,-4.547008547008545,-3.9892858012516115,-7.814553897296733,-9.68546703354523,-11.758855194041395,-9.500012810452446,-6.02656065453003,-2.678111693000716,-7.762054965956388,-4.955890237178558,-3.179483224749447,-2.070566240940323,-6.6026618662643415,-3.776210725235984,-2.4759859198692027,-4.859274338230051,-4.038668120840633,-5.264248228346467,-8.717175683191812,-11.004031373646804,-11.633288323448895,-9.600560425733171,-10.13217748107575,-7.4816532084116645,-5.8781751804450835,-7.825556495508927,-3.4387406426898934,-0.20190496720273643,2.256606231167005,5.1941109452040735,-0.2217370531452758,0.8823334558761218,-3.766982623447589,-5.29260653034283,-7.863478747649999,-3.166999957817737,-1.365861305295958,-4.64570012391512]; + var verified_macd12v26v9signal = [0,-0.9094017094017091,-1.5253785277716898,-2.7832136016766986,-4.163664288050406,-5.6827024692486034,-6.446164537489373,-6.362243760897504,-5.625417347318147,-6.052744871045796,-5.833373944272349,-5.30259580036777,-4.65618988848228,-5.045484284038693,-4.791629572278152,-4.328500841796362,-4.4346555410831,-4.355458057034607,-4.537216091296979,-5.373208009675945,-6.499372682470117,-7.526155810665873,-7.941036733679333,-8.379264883158616,-8.199742548209226,-7.735429074656398,-7.753454558826904,-6.890511775599502,-5.55279041392015,-3.990911084902719,-2.153906678881361,-1.767472753734144,-1.2375115118120907,-1.7434057341391904,-2.4532458933799184,-3.535292464233935,-3.4616339629506956,-3.0424794314197485,-3.363123569918823]; + var verified_macd12v26v9result = [0,-3.6376068376068362,-2.4639072734799217,-5.031340295620034,-5.521802745494824,-6.076152724792792,-3.053848272963074,0.33568310636747434,2.9473056543174314,-1.7093100949105917,0.8774837070937913,2.1231125756183227,2.585623647541957,-1.5571775822256484,1.0154188470421683,1.8525149219271597,-0.42461879714695083,0.3167899361939739,-0.7270321370494885,-3.3439676735158663,-4.504658691176687,-4.107132512783021,-1.6595236920538383,-1.7529125979171347,0.7180893397975616,1.8572538942113148,-0.07210193668202258,3.4517711329096086,5.350885446717413,6.247517316069724,7.3480176240854345,1.5457357005888681,2.1198449676882127,-2.0235768893083987,-2.839360636962912,-4.3281862834160645,0.2946340051329588,1.6766181261237905,-1.2825765539962966]; + + it('should correctly calculate MACD diffs with 12/26/9', function() { + var macd = new MACD({short: 12, long: 26, signal: 9}); + _.each(prices, function(p, i) { + macd.update(p); + expect(macd.diff).to.equal(verified_macd12v26v9diff[i]); + }); }); - test.done(); - } -}; - -// MACD results - -var verified_macd12v26v9diff = [0,-4.547008547008545,-3.9892858012516115,-7.814553897296733,-9.68546703354523,-11.758855194041395,-9.500012810452446,-6.02656065453003,-2.678111693000716,-7.762054965956388,-4.955890237178558,-3.179483224749447,-2.070566240940323,-6.6026618662643415,-3.776210725235984,-2.4759859198692027,-4.859274338230051,-4.038668120840633,-5.264248228346467,-8.717175683191812,-11.004031373646804,-11.633288323448895,-9.600560425733171,-10.13217748107575,-7.4816532084116645,-5.8781751804450835,-7.825556495508927,-3.4387406426898934,-0.20190496720273643,2.256606231167005,5.1941109452040735,-0.2217370531452758,0.8823334558761218,-3.766982623447589,-5.29260653034283,-7.863478747649999,-3.166999957817737,-1.365861305295958,-4.64570012391512]; -var verified_macd12v26v9signal = [0,-0.9094017094017091,-1.5253785277716898,-2.7832136016766986,-4.163664288050406,-5.6827024692486034,-6.446164537489373,-6.362243760897504,-5.625417347318147,-6.052744871045796,-5.833373944272349,-5.30259580036777,-4.65618988848228,-5.045484284038693,-4.791629572278152,-4.328500841796362,-4.4346555410831,-4.355458057034607,-4.537216091296979,-5.373208009675945,-6.499372682470117,-7.526155810665873,-7.941036733679333,-8.379264883158616,-8.199742548209226,-7.735429074656398,-7.753454558826904,-6.890511775599502,-5.55279041392015,-3.990911084902719,-2.153906678881361,-1.767472753734144,-1.2375115118120907,-1.7434057341391904,-2.4532458933799184,-3.535292464233935,-3.4616339629506956,-3.0424794314197485,-3.363123569918823]; -var verified_macd12v26v9result = [0,-3.6376068376068362,-2.4639072734799217,-5.031340295620034,-5.521802745494824,-6.076152724792792,-3.053848272963074,0.33568310636747434,2.9473056543174314,-1.7093100949105917,0.8774837070937913,2.1231125756183227,2.585623647541957,-1.5571775822256484,1.0154188470421683,1.8525149219271597,-0.42461879714695083,0.3167899361939739,-0.7270321370494885,-3.3439676735158663,-4.504658691176687,-4.107132512783021,-1.6595236920538383,-1.7529125979171347,0.7180893397975616,1.8572538942113148,-0.07210193668202258,3.4517711329096086,5.350885446717413,6.247517316069724,7.3480176240854345,1.5457357005888681,2.1198449676882127,-2.0235768893083987,-2.839360636962912,-4.3281862834160645,0.2946340051329588,1.6766181261237905,-1.2825765539962966]; - -test.macd = { - _12v26v9_diff: function(test) { - var macd = new MACD({short: 12, long: 26, signal: 9}); - _.each(prices, function(p, i) { - macd.update(p); - test.equals(macd.diff, verified_macd12v26v9diff[i]); + + it('should correctly calculate MACD signals with 12/26/9', function() { + var macd = new MACD({short: 12, long: 26, signal: 9}); + _.each(prices, function(p, i) { + macd.update(p); + expect(macd.signal.result).to.equal(verified_macd12v26v9signal[i]); + }); }); - test.done(); - }, - _12v26v9_signal: function(test) { - var macd = new MACD({short: 12, long: 26, signal: 9}); - _.each(prices, function(p, i) { - macd.update(p); - test.equals(macd.signal.result, verified_macd12v26v9signal[i]); + + it('should correctly calculate MACD results with 12/26/9', function() { + var macd = new MACD({short: 12, long: 26, signal: 9}); + _.each(prices, function(p, i) { + macd.update(p); + expect(macd.result).to.equal(verified_macd12v26v9result[i]); + }); }); - test.done(); - }, - _12v26v9_result: function(test) { - var macd = new MACD({short: 12, long: 26, signal: 9}); - _.each(prices, function(p, i) { - macd.update(p); - test.equals(macd.result, verified_macd12v26v9result[i]); + + }); + + describe('PPO', function() { + + var PPO = require('../methods/indicators/PPO.js'); + + var verified_ppo12v26v9 = [0,-5.922297673383055,-5.204813152773915,-10.775104631720897,-13.901816018390623,-17.719369436924076,-14.22599518036208,-8.779001074074772,-3.7775327599722406,-11.79779911287454,-7.3650541223130555,-4.659894802610166,-3.0092688006197794,-10.268186733549467,-5.72296687408477,-3.714833010426942,-7.582596159036941,-6.295429716477546,-8.431243146403258,-14.906489369094212,-19.9673059077387,-21.968696242678835,-18.053100351760996,-19.745285752660987,-14.297565938958984,-11.143819939485432,-15.619787658403148,-6.481364924591247,-0.3633332042639952,3.9025254618963814,8.5325172756507,-0.3918493405267646,1.5323382353294481,-7.05563807597937,-10.289915270521151,-16.28333826120637,-6.14602662946988,-2.589345706109427,-9.383623293044062]; + var verified_ppo12v26v9signal = [0,-1.184459534676611,-1.9885302582960718,-3.745845132981037,-5.777039310062954,-8.16550533543518,-9.37760330442056,-9.257882858351403,-8.161812838675571,-8.889010093515365,-8.584218899274903,-7.799354079941956,-6.8413370240775215,-7.526706965971911,-7.165958947594484,-6.475733760160976,-6.697106239936169,-6.616770935244445,-6.979665377476208,-8.565030175799809,-10.845485322187589,-13.07012750628584,-14.066722075380872,-15.202434810836897,-15.021461036461314,-14.245932817066137,-14.52070378533354,-12.912836013185082,-10.402935451400865,-7.541843268741416,-4.3269711598629925,-3.539946795995747,-2.5254897897307083,-3.431519446980441,-4.8031986116885825,-7.099226541592141,-6.908586559167689,-6.044738388556036,-6.712515369453642]; + var verified_ppo12v26v9hist = [0,-4.737838138706444,-3.2162828944778434,-7.02925949873986,-8.12477670832767,-9.553864101488896,-4.84839187594152,0.47888178427663064,4.3842800787033305,-2.9087890193591743,1.2191647769618479,3.13945927733179,3.832068223457742,-2.7414797675775553,1.4429920735097133,2.760900749734034,-0.8854899191007712,0.32134121876689914,-1.4515777689270495,-6.341459193294403,-9.121820585551113,-8.898568736392996,-3.9863782763801243,-4.54285094182409,0.7238950975023304,3.102112877580705,-1.099083873069608,6.431471088593835,10.03960224713687,11.444368730637798,12.859488435513693,3.1480974554689825,4.057828025060156,-3.6241186289989296,-5.486716658832568,-9.18411171961423,0.762559929697809,3.455392682446609,-2.6711079235904203]; + + it('should correctly calculate PPOs with 12/26/9', function() { + var ppo = new PPO({short: 12, long: 26, signal: 9}); + _.each(prices, function(p, i) { + ppo.update(p); + expect(ppo.ppo).to.equal(verified_ppo12v26v9[i]); + }); }); - test.done(); - } -}; - -// PPO test results - -var verified_ppo12v26v9 = [0,-5.922297673383055,-5.204813152773915,-10.775104631720897,-13.901816018390623,-17.719369436924076,-14.22599518036208,-8.779001074074772,-3.7775327599722406,-11.79779911287454,-7.3650541223130555,-4.659894802610166,-3.0092688006197794,-10.268186733549467,-5.72296687408477,-3.714833010426942,-7.582596159036941,-6.295429716477546,-8.431243146403258,-14.906489369094212,-19.9673059077387,-21.968696242678835,-18.053100351760996,-19.745285752660987,-14.297565938958984,-11.143819939485432,-15.619787658403148,-6.481364924591247,-0.3633332042639952,3.9025254618963814,8.5325172756507,-0.3918493405267646,1.5323382353294481,-7.05563807597937,-10.289915270521151,-16.28333826120637,-6.14602662946988,-2.589345706109427,-9.383623293044062]; -var verified_ppod12v26v9signal = [0,-1.184459534676611,-1.9885302582960718,-3.745845132981037,-5.777039310062954,-8.16550533543518,-9.37760330442056,-9.257882858351403,-8.161812838675571,-8.889010093515365,-8.584218899274903,-7.799354079941956,-6.8413370240775215,-7.526706965971911,-7.165958947594484,-6.475733760160976,-6.697106239936169,-6.616770935244445,-6.979665377476208,-8.565030175799809,-10.845485322187589,-13.07012750628584,-14.066722075380872,-15.202434810836897,-15.021461036461314,-14.245932817066137,-14.52070378533354,-12.912836013185082,-10.402935451400865,-7.541843268741416,-4.3269711598629925,-3.539946795995747,-2.5254897897307083,-3.431519446980441,-4.8031986116885825,-7.099226541592141,-6.908586559167689,-6.044738388556036,-6.712515369453642]; -var verified_ppod12v26v9hist = [0,-4.737838138706444,-3.2162828944778434,-7.02925949873986,-8.12477670832767,-9.553864101488896,-4.84839187594152,0.47888178427663064,4.3842800787033305,-2.9087890193591743,1.2191647769618479,3.13945927733179,3.832068223457742,-2.7414797675775553,1.4429920735097133,2.760900749734034,-0.8854899191007712,0.32134121876689914,-1.4515777689270495,-6.341459193294403,-9.121820585551113,-8.898568736392996,-3.9863782763801243,-4.54285094182409,0.7238950975023304,3.102112877580705,-1.099083873069608,6.431471088593835,10.03960224713687,11.444368730637798,12.859488435513693,3.1480974554689825,4.057828025060156,-3.6241186289989296,-5.486716658832568,-9.18411171961423,0.762559929697809,3.455392682446609,-2.6711079235904203]; - -test.ppo = { - _12v26v9: function(test) { - var ppo = new PPO({short: 12, long: 26, signal: 9}); - _.each(prices, function(p, i) { - ppo.update(p); - test.equals(ppo.ppo, verified_ppo12v26v9[i]); + + it('should correctly calculate PPO signals with 12/26/9', function() { + var ppo = new PPO({short: 12, long: 26, signal: 9}); + _.each(prices, function(p, i) { + ppo.update(p); + expect(ppo.PPOsignal.result).to.equal(verified_ppo12v26v9signal[i]); + }); }); - test.done(); - }, - _12v26v9_signal: function(test) { - var ppo = new PPO({short: 12, long: 26, signal: 9}); - _.each(prices, function(p, i) { - ppo.update(p); - test.equals(ppo.PPOsignal.result, verified_ppod12v26v9signal[i]); + + + it('should correctly calculate PPO hists with 12/26/9', function() { + var ppo = new PPO({short: 12, long: 26, signal: 9}); + _.each(prices, function(p, i) { + ppo.update(p); + expect(ppo.PPOhist).to.equal(verified_ppo12v26v9hist[i]); + }); }); - test.done(); - }, - _12v26v9_hist: function(test) { - var ppo = new PPO({short: 12, long: 26, signal: 9}); - _.each(prices, function(p, i) { - ppo.update(p); - test.equals(ppo.PPOhist, verified_ppod12v26v9hist[i]); + }); + + xdescribe('DEMA', function() { + + var DEMA = require('../methods/indicators/DEMA.js'); + + xit('should correctly calculate DEMAs', function() { + // TODO! }); - test.done(); - } -}; -module.exports = test; \ No newline at end of file + + }); + +}); \ No newline at end of file diff --git a/test/tradeBatcher.js b/test/tradeBatcher.js new file mode 100644 index 000000000..e08e0e072 --- /dev/null +++ b/test/tradeBatcher.js @@ -0,0 +1,119 @@ +var chai = require('chai'); +var expect = chai.expect; +var should = chai.should; +var sinon = require('sinon'); + +var _ = require('lodash'); +var moment = require('moment'); + +var utils = require(__dirname + '/../core/util'); +var dirs = utils.dirs(); +var TradeBatcher = require(dirs.budfox + 'tradeBatcher'); + +var trades_tid_1 = [ + {tid: 1, price: 10, amount: 1, date: 1466115793}, + {tid: 2, price: 10, amount: 1, date: 1466115794}, + {tid: 3, price: 10, amount: 1, date: 1466115795} +]; + +var trades_tid_2 = [ + {tid: 2, price: 10, amount: 1, date: 1466115794}, + {tid: 3, price: 10, amount: 1, date: 1466115795}, + {tid: 4, price: 10, amount: 1, date: 1466115796}, + {tid: 5, price: 10, amount: 1, date: 1466115797} +]; + +describe('tradeBatcher', function() { + var tb; + + it('should throw when not passed a number', function() { + expect(function() { + new TradeBatcher() + }).to.throw('tid is not a string'); + }); + + it('should instantiate', function() { + tb = new TradeBatcher('tid'); + }); + + it('should throw when not fed an array', function() { + var trade = _.first(trades_tid_1); + expect( + tb.write.bind(tb, trade) + ).to.throw('batch is not an array'); + }); + + it('should emit an event when fed trades', function() { + tb = new TradeBatcher('tid'); + + var spy = sinon.spy(); + tb.on('new batch', spy); + tb.write( trades_tid_1 ); + expect(spy.callCount).to.equal(1); + }); + + it('should only emit once when fed the same trades twice', function() { + tb = new TradeBatcher('tid'); + + var spy = sinon.spy(); + tb.on('new batch', spy); + tb.write( trades_tid_1 ); + tb.write( trades_tid_1 ); + expect(spy.callCount).to.equal(1); + }); + + it('should correctly set meta data', function() { + tb = new TradeBatcher('tid'); + + var spy = sinon.spy(); + tb.on('new batch', spy); + + tb.write( trades_tid_1 ); + + var transformedTrades = _.map(_.cloneDeep(trades_tid_1), function(trade) { + trade.date = moment.unix(trade.date).utc(); + return trade; + }); + + var result = { + data: transformedTrades, + amount: _.size(transformedTrades), + start: _.first(transformedTrades).date, + end: _.last(transformedTrades).date, + first: _.first(transformedTrades), + last: _.last(transformedTrades) + } + + var tbResult = _.first(_.first(spy.args)); + expect(tbResult.amount).to.equal(result.amount); + expect(tbResult.start.unix()).to.equal(result.start.unix()); + expect(tbResult.end.unix()).to.equal(result.end.unix()); + expect(tbResult.data.length).to.equal(result.data.length); + + _.each(tbResult.data, function(t, i) { + expect(tbResult.data[i].tid).to.equal(result.data[i].tid); + expect(tbResult.data[i].price).to.equal(result.data[i].price); + expect(tbResult.data[i].amount).to.equal(result.data[i].amount); + }); + }); + +it('should correctly filter trades', function() { + tb = new TradeBatcher('tid'); + + var spy = sinon.spy(); + tb.on('new batch', spy); + + tb.write( trades_tid_1 ); + tb.write( trades_tid_2 ); + + expect(spy.callCount).to.equal(2); + + var tbResult = _.first(_.last(spy.args)); + + expect(tbResult.amount).to.equal(2); + expect(tbResult.start.unix()).to.equal(1466115796); + expect(tbResult.end.unix()).to.equal(1466115797); + expect(tbResult.data.length).to.equal(2); +}); + +}); \ No newline at end of file diff --git a/test/tradeFetcher.js b/test/tradeFetcher.js index 1d2104fb6..85b5a2ac9 100644 --- a/test/tradeFetcher.js +++ b/test/tradeFetcher.js @@ -1,66 +1,68 @@ -var _ = require('lodash'); -var moment = require('moment'); +// TODO: rewrite tests for mocha once we use Days. -var util = require('../core/util'); +// var _ = require('lodash'); +// var moment = require('moment'); -var Fetcher = require('../core/tradeFetcher'); +// var util = require('../core/util'); -var TRADES = [ - { tid: 4, amount: 1.4, price: 99, date: 1381356250 }, - { tid: 5, amount: 1.5, price: 98, date: 1381356310 }, - { tid: 6, amount: 1.6, price: 97, date: 1381356370 }, - { tid: 7, amount: 1.7, price: 96, date: 1381356430 }, - { tid: 8, amount: 1.8, price: 95, date: 1381356490 }, - { tid: 9, amount: 1.9, price: 94, date: 1381356550 }, - { tid: 10, amount: 2, price: 93, date: 1381356610 }, - { tid: 11, amount: 2.1, price: 92, date: 1381356670 }, - { tid: 12, amount: 2.2, price: 91, date: 1381356730 }, - { tid: 13, amount: 2.3, price: 90, date: 1381356790 } -]; +// var Fetcher = require('../core/tradeFetcher'); -var FakeFetcher = function(cb) { - this.exchange = { - providesHistory: true - } -} -FakeFetcher.prototype = { - calculateNextFetch: function() {} -} -// the methods we want to test -FakeFetcher.prototype.processTrades = Fetcher.prototype.processTrades; -FakeFetcher.prototype.setFetchMeta = Fetcher.prototype.setFetchMeta; +// var TRADES = [ +// { tid: 4, amount: 1.4, price: 99, date: 1381356250 }, +// { tid: 5, amount: 1.5, price: 98, date: 1381356310 }, +// { tid: 6, amount: 1.6, price: 97, date: 1381356370 }, +// { tid: 7, amount: 1.7, price: 96, date: 1381356430 }, +// { tid: 8, amount: 1.8, price: 95, date: 1381356490 }, +// { tid: 9, amount: 1.9, price: 94, date: 1381356550 }, +// { tid: 10, amount: 2, price: 93, date: 1381356610 }, +// { tid: 11, amount: 2.1, price: 92, date: 1381356670 }, +// { tid: 12, amount: 2.2, price: 91, date: 1381356730 }, +// { tid: 13, amount: 2.3, price: 90, date: 1381356790 } +// ]; -module.exports = { - processTrades: function(test) { - var checker = function(what, data) { - // should broadcast the `new trades` - // event - test.equal(what, 'new trades'); +// var FakeFetcher = function(cb) { +// this.exchange = { +// providesHistory: true +// } +// } +// FakeFetcher.prototype = { +// calculateNextFetch: function() {} +// } +// // the methods we want to test +// FakeFetcher.prototype.processTrades = Fetcher.prototype.processTrades; +// FakeFetcher.prototype.setFetchMeta = Fetcher.prototype.setFetchMeta; - // every trade should be in there in - // the same order - _.each(TRADES, function(t, i) { - test.deepEqual(t, data.all[i]); - }); +// module.exports = { +// processTrades: function(test) { +// var checker = function(what, data) { +// // should broadcast the `new trades` +// // event +// test.equal(what, 'new trades'); - // meta should be correct - test.deepEqual(_.first(TRADES), data.first); - test.deepEqual(_.last(TRADES), data.last); +// // every trade should be in there in +// // the same order +// _.each(TRADES, function(t, i) { +// test.deepEqual(t, data.all[i]); +// }); - test.ok(util.equals(moment.unix(1381356250), data.start)); - test.ok(util.equals(moment.unix(1381356790), data.end)); +// // meta should be correct +// test.deepEqual(_.first(TRADES), data.first); +// test.deepEqual(_.last(TRADES), data.last); - test.equal( - data.timespan, - 1381356790 * 1000 - 1381356250 * 1000 - ); +// test.ok(util.equals(moment.unix(1381356250), data.start)); +// test.ok(util.equals(moment.unix(1381356790), data.end)); - test.done(); - } +// test.equal( +// data.timespan, +// 1381356790 * 1000 - 1381356250 * 1000 +// ); - var fetcher = new FakeFetcher; - fetcher.emit = checker; - fetcher.processTrades(false, TRADES); +// test.done(); +// } - } -} \ No newline at end of file +// var fetcher = new FakeFetcher; +// fetcher.emit = checker; +// fetcher.processTrades(false, TRADES); + +// } +// } \ No newline at end of file diff --git a/web/frontend/index.html b/web/frontend/index.html index fbf413f90..6cab0f7b6 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -1,108 +1,58 @@ - + + Gekko prototype + -

Gekko V0.0.0.1!

-

{{MARKET}}

-
- -
-
+

Gekko (early prototype)

+

advices

+

+  

trade data

+

   
   
-  
-  
-  
+  
+  
+  
   
-  
-  
-  
 
-
\ No newline at end of file
+
diff --git a/web/server.js b/web/server.js
index 506f00974..82fa3ef19 100644
--- a/web/server.js
+++ b/web/server.js
@@ -1,23 +1,24 @@
-// 
+//
 // Current state: early prototype
 // 
-// todo: express maybe?
+// todo: koa maybe?
 // 
 
-// 
+//
 // Spawn a nodejs webserver
-// 
+//
 
 var _ = require('lodash');
 var async = require('async');
-var config = require('../core/util').getConfig();
+var config = _.cloneDeep(require('../core/util').getConfig());
 // we are going to send it to web clients, remove
 // potential private information
 delete config.mailer;
+delete config.trader
 
 var serverConfig = config.webserver;
 
-var ws = require("nodejs-websocket");
+var ws = require("zwebsocket");
 var http = require("http");
 var fs = require('fs');
 
@@ -25,16 +26,8 @@ var Server = function() {
   _.bindAll(this);
 
   this.history = false;
+  this.advices = false;
   this.index;
-
-  // static assets Gekko
-  // can pass 
-  this.assets = [
-    '/css/style.css',
-    '/js/d3.chart.js',
-    '/js/d3.candlechart.js',
-    '/js/main.js'
-  ]
 }
 
 Server.prototype.setup = function(next) {
@@ -69,22 +62,43 @@ Server.prototype.setupHTTP = function(next) {
     .listen(serverConfig.http.port, next);
 }
 
-Server.prototype.broadcastHistory = function(data) {
-  this.history = data;
-  this.broadcast({
-    message: 'history',
-    data: data
-  });
-}
+// Server.prototype.broadcastHistory = function(data) {
+//   this.history = data;
+//   this.broadcast({
+//     message: 'history',
+//     data: data
+//   });
+// }
+
+Server.prototype.broadcastCandle = function(_candle) {
+  var candle = _.clone(_candle);
+  candle.start = candle.start.unix();
+
+  if(!this.history)
+    this.history = [];
+
+  this.history.push(candle);
+
+  if(_.size(this.history) > 1000)
+    this.history.shift();
 
-Server.prototype.broadcastSmallCandle = function(candle) {
   this.broadcast({
     message: 'candle',
     data: candle
   });
 }
 
-Server.prototype.broadcastAdvice = function() {}
+Server.prototype.broadcastAdvice = function(advice) {
+  if(!this.advices)
+    this.advices = [];
+
+  this.advices.push(advice);
+
+  this.broadcast({
+    message: 'advice',
+    data: advice
+  });
+}
 
 Server.prototype.broadcastTrade = function(trade) {
   this.broadcast({
@@ -95,6 +109,8 @@ Server.prototype.broadcastTrade = function(trade) {
 
 Server.prototype.handleHTTPConnection = function(req, res) {
 
+  console.log(req.url);
+
   if(req.url === '/') {
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end(this.index);
@@ -124,11 +140,23 @@ Server.prototype.broadcast = function(obj) {
 }
 
 Server.prototype.handleWSConnection = function(conn) {
-  if(this.history)
+  console.log('new con');
+  if(this.history) {
+    console.log('sending history', _.size(this.history))
     this.send(conn, {
       message: 'history',
       data: this.history
     });
+  }
+
+  if(this.advices) {
+    _.each(this.advices, function(a) {
+      this.send(conn, {
+        message: 'advice',
+        data: a
+      });
+    }, this)
+  }
 
   this.send(conn, {
     message: 'config',
@@ -145,4 +173,4 @@ Server.prototype.handleWSConnection = function(conn) {
   conn.on("close", function(code, reason) {});
   conn.on("error", function(code, reason) {});
 }
-module.exports = Server;
\ No newline at end of file
+module.exports = Server;