This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2021-06-06 20:25:14 +02:00

335 lines
8.0 KiB
JavaScript

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;