const _ = require('lodash'); const fs = require('fs'); const util = require('../../core/util'); const config = util.getConfig(); const dirs = util.dirs(); const log = require(dirs.core + 'log'); const ENV = util.gekkoEnv(); const mode = util.gekkoMode(); const startTime = util.getStartTime(); const indicatorsPath = dirs.methods + 'indicators/'; const indicatorFiles = fs.readdirSync(indicatorsPath); const Indicators = {}; const AsyncIndicatorRunner = require('./asyncIndicatorRunner'); _.each(indicatorFiles, function(indicator) { const indicatorName = indicator.split(".")[0]; if (indicatorName[0] != "_") try { Indicators[indicatorName] = require(indicatorsPath + indicator); } catch (e) { log.error("Failed to load indicator", indicatorName); } }); const allowedIndicators = _.keys(Indicators); var Base = function(settings) { _.bindAll(this); // properties this.age = 0; this.processedTicks = 0; this.setup = false; this.settings = settings; this.tradingAdvisor = config.tradingAdvisor; // defaults this.priceValue = 'close'; this.indicators = {}; this.asyncTick = false; this.deferredTicks = []; this.propogatedAdvices = 0; this.completedWarmup = false; this.asyncIndicatorRunner = new AsyncIndicatorRunner(); this._currentDirection; // make sure we have all methods _.each(['init', 'check'], function(fn) { if(!this[fn]) util.die('No ' + fn + ' function in this strategy found.') }, this); if(!this.update) this.update = function() {}; if(!this.end) this.end = function() {}; if(!this.onTrade) this.onTrade = function() {}; // let's run the implemented starting point this.init(); if(_.isNumber(this.requiredHistory)) { log.debug('Ignoring strategy\'s required history, using the "config.tradingAdvisor.historySize" instead.'); } this.requiredHistory = config.tradingAdvisor.historySize; if(!config.debug || !this.log) this.log = function() {}; this.setup = true; if(_.size(this.asyncIndicatorRunner.talibIndicators) || _.size(this.asyncIndicatorRunner.tulipIndicators)) this.asyncTick = true; else delete this.asyncIndicatorRunner; } // teach our base trading method events util.makeEventEmitter(Base); Base.prototype.tick = function(candle, done) { this.age++; const afterAsync = () => { this.calculateSyncIndicators(candle, done); } if(this.asyncTick) { this.asyncIndicatorRunner.processCandle(candle, () => { if(!this.talibIndicators) { this.talibIndicators = this.asyncIndicatorRunner.talibIndicators; this.tulipIndicators = this.asyncIndicatorRunner.tulipIndicators; } afterAsync(); }); } else { afterAsync(); } } Base.prototype.isBusy = function() { if(!this.asyncTick) return false; return this.asyncIndicatorRunner.inflight; } Base.prototype.calculateSyncIndicators = function(candle, done) { // update all indicators var price = candle[this.priceValue]; _.each(this.indicators, function(i) { if(i.input === 'price') i.update(price); if(i.input === 'candle') i.update(candle); },this); this.propogateTick(candle); return done(); } Base.prototype.propogateTick = function(candle) { this.candle = candle; this.update(candle); this.processedTicks++; var isAllowedToCheck = this.requiredHistory <= this.age; if(!this.completedWarmup) { // in live mode we might receive more candles // than minimally needed. In that case check // whether candle start time is > startTime var isPremature = false; if(mode === 'realtime') { const startTimeMinusCandleSize = startTime .clone() .subtract(this.tradingAdvisor.candleSize, "minutes"); isPremature = candle.start < startTimeMinusCandleSize; } if(isAllowedToCheck && !isPremature) { this.completedWarmup = true; this.emit( 'stratWarmupCompleted', {start: candle.start.clone()} ); } } if(this.completedWarmup) { this.log(candle); this.check(candle); if( this.asyncTick && this.hasSyncIndicators && this.deferredTicks.length ) { return this.tick(this.deferredTicks.shift()) } } const indicators = {}; _.each(this.indicators, (indicator, name) => { indicators[name] = indicator.result; }); _.each(this.tulipIndicators, (indicator, name) => { indicators[name] = indicator.result.result ? indicator.result.result : indicator.result; }); _.each(this.talibIndicators, (indicator, name) => { indicators[name] = indicator.result.outReal ? indicator.result.outReal : indicator.result; }); this.emit('stratUpdate', { date: candle.start.clone(), indicators }); // are we totally finished? const completed = this.age === this.processedTicks; if(completed && this.finishCb) this.finishCb(); } Base.prototype.processTrade = function(trade) { if( this._pendingTriggerAdvice && trade.action === 'sell' && this._pendingTriggerAdvice === trade.adviceId ) { // This trade came from a trigger of the previous advice, // update stored direction this._currentDirection = 'short'; this._pendingTriggerAdvice = null; } this.onTrade(trade); } Base.prototype.addTalibIndicator = function(name, type, parameters) { this.asyncIndicatorRunner.addTalibIndicator(name, type, parameters); } Base.prototype.addTulipIndicator = function(name, type, parameters) { this.asyncIndicatorRunner.addTulipIndicator(name, type, parameters); } Base.prototype.addIndicator = function(name, type, parameters) { if(!_.contains(allowedIndicators, type)) util.die('I do not know the indicator ' + type); if(this.setup) util.die('Can only add indicators in the init method!'); return this.indicators[name] = new Indicators[type](parameters); // some indicators need a price stream, others need full candles } Base.prototype.advice = function(newDirection) { // ignore legacy soft advice if(!newDirection) { return; } let trigger; if(_.isObject(newDirection)) { if(!_.isString(newDirection.direction)) { log.error('Strategy emitted unparsable advice:', newDirection); return; } if(newDirection.direction === this._currentDirection) { return; } if(_.isObject(newDirection.trigger)) { if(newDirection.direction !== 'long') { log.warn( 'Strategy adviced a stop on not long, this is not supported.', 'As such the stop is ignored' ); } else { // the trigger is implemented in a trader trigger = newDirection.trigger; if(trigger.trailPercentage && !trigger.trailValue) { trigger.trailValue = trigger.trailPercentage / 100 * this.candle.close; log.info('[StratRunner] Trailing stop trail value specified as percentage, setting to:', trigger.trailValue); } } } newDirection = newDirection.direction; } if(newDirection === this._currentDirection) { return; } if(newDirection === 'short' && this._pendingTriggerAdvice) { this._pendingTriggerAdvice = null; } this._currentDirection = newDirection; this.propogatedAdvices++; const advice = { id: 'advice-' + this.propogatedAdvices, recommendation: newDirection }; if(trigger) { advice.trigger = trigger; this._pendingTriggerAdvice = 'advice-' + this.propogatedAdvices; } else { this._pendingTriggerAdvice = null; } this.emit('advice', advice); return this.propogatedAdvices; } Base.prototype.notify = function(content) { this.emit('stratNotification', { content, date: new Date(), }) } Base.prototype.finish = function(done) { // Because the strategy might be async we need // to be sure we only stop after all candles are // processed. if(!this.asyncTick) { this.end(); return done(); } if(this.age === this.processedTicks) { this.end(); return done(); } // we are not done, register cb // and call after we are.. this.finishCb = done; } module.exports = Base;