bluebot
Version:
A bitcoin trading bot for auto trading at various exchanges
217 lines (175 loc) • 7 kB
JavaScript
var _ = require('lodash');
var fs = require('fs');
var moment = require('moment');
var util = require('../../core/util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + '/log');
var Stitcher = function(batcher) {
this.batcher = batcher;
}
Stitcher.prototype.ago = function(ts) {
var now = moment().utc();
var then = moment.unix(ts).utc();
return now.diff(then, 'minutes') + ' minutes ago';
}
Stitcher.prototype.verifyExchange = function() {
var exchangeChecker = require(dirs.core + 'exchangeChecker');
var slug = config.watch.exchange.toLowerCase();
var exchange = exchangeChecker.getExchangeCapabilities(slug);
if(!exchange)
util.die(`Unsupported exchange: ${slug}`);
var error = exchangeChecker.cantMonitor(config.watch);
if(error)
util.die(error, true);
}
Stitcher.prototype.prepareHistoricalData = function(done) {
this.verifyExchange();
// - step 1: check most recent stored candle window
// - step 2: check oldest trade reachable by API
// - step 3: see if overlap
// - step 4: feed candle stream into CandleBatcher
if(config.tradingAdvisor.historySize === 0)
return done();
var requiredHistory = config.tradingAdvisor.candleSize * config.tradingAdvisor.historySize;
var Reader = require(dirs.plugins + config.adapter + '/reader');
this.reader = new Reader;
log.info(
'\tThe trading method requests',
requiredHistory,
'minutes of historic data. Checking availablity..'
);
var endTime = moment().utc().startOf('minute');
var idealStartTime = endTime.clone().subtract('m', requiredHistory);
this.reader.mostRecentWindow(idealStartTime, endTime, function(localData) {
// now we know what data is locally available, what
// data would we need from the exchange?
if(!localData) {
log.info('\tNo usable local data available, trying to get as much as possible from the exchange..');
var idealExchangeStartTime = idealStartTime.clone();
var idealExchangeStartTimeTS = idealExchangeStartTime.unix();
}
else {
log.debug('\tAvailable local data:');
log.debug('\t\tfrom:', this.ago(localData.from));
log.debug('\t\tto:', this.ago(localData.to));
log.info('\tUsable local data available, trying to match with exchange data..')
// local data is available, we need the next minute
// make sure we grab back in history far enough
var secondsOverlap = 60 * 15; // 15 minutes
var idealExchangeStartTimeTS = localData.to - secondsOverlap;
var idealExchangeStartTime = moment.unix(idealExchangeStartTimeTS).utc();
// already set the
util.setConfigProperty(
'tradingAdvisor',
'firstFetchSince',
idealExchangeStartTimeTS
);
}
// Limit the history BlueBot can try to get from the exchange.
var minutesAgo = endTime.diff(idealExchangeStartTime, 'minutes');
var maxMinutesAgo = 4 * 60; // 4 hours
if(minutesAgo > maxMinutesAgo) {
log.info('\tPreventing BlueBot from requesting', minutesAgo, 'minutes of history.');
idealExchangeStartTime = endTime.clone().subtract('minutes', maxMinutesAgo);
idealExchangeStartTimeTS = idealExchangeStartTime.unix();
}
log.debug('\tFetching exchange data since', this.ago(idealExchangeStartTimeTS))
this.checkExchangeTrades(idealExchangeStartTime, function(err, exchangeData) {
log.debug('\tAvailable exchange data:');
log.debug('\t\tfrom:', this.ago(exchangeData.from));
log.debug('\t\tto:', this.ago(exchangeData.to));
// in case we have limited local data, and the
// exchange would offer more than we have: ignore
// the local data..
if(
localData &&
exchangeData &&
exchangeData.from < localData.from
) {
log.debug('\tExchange offered more data than locally available. Ignoring local data');
localData = false;
}
var stitchable = localData && exchangeData.from <= localData.to;
if(stitchable) {
log.debug('\tStitching datasets');
// we can combine local data with exchange data
if(idealStartTime.unix() >= localData.from) {
log.info(
'\tFull history locally available.',
'Seeding the trading method with all required historical candles.'
);
} else {
// stitchable but not enough
log.info(
'\tPartial history locally available, but',
Math.round((localData.from - idealStartTime.unix()) / 60),
'minutes are missing.')
log.info('\tSeeding the trading method with',
'partial historical data (BlueBot needs more time before',
'it can give advice).'
);
}
// seed all historic data up to the point the exchange can provide.
var from = localData.from;
var to = moment.unix(exchangeData.from).utc()
.startOf('minute')
.subtract('minute', 1)
.unix();
log.debug('\tSeeding with:');
log.debug('\t\tfrom:', this.ago(from));
log.debug('\t\tto:', this.ago(to));
return this.seedLocalData(from, to, done);
} else if(!stitchable) {
log.debug('\tUnable to stitch datasets.')
// we cannot use any local data..
log.info(
'\tNot seeding locally available data to the trading method.'
);
if(exchangeData.from < idealExchangeStartTimeTS) {
log.info('\tHowever the exchange returned enough data anyway!');
} else if(localData) {
log.info(
'\tThe exchange does not return enough data.',
Math.round((localData.from - idealStartTime.unix()) / 60),
'minutes are still missing.'
);
}
}
done();
}.bind(this));
}.bind(this));
}
Stitcher.prototype.checkExchangeTrades = function(since, next) {
var provider = config.watch.exchange.toLowerCase();
var DataProvider = require(util.dirs().bluebot + 'exchanges/' + provider);
var exchangeConfig = config.watch;
// include trader config if trading is enabled
if (_.isObject(config.trader) && config.trader.enabled) {
exchangeConfig = _.extend(config.watch, config.trader);
}
var watcher = new DataProvider(exchangeConfig);
watcher.getTrades(since, function(e, d) {
if(_.isEmpty(d))
return util.die(
`BlueBot tried to retrieve data since ${since.format('YYYY-MM-DD HH:mm:ss')}, however
${provider} did not return any trades.`
);
next(e, {
from: _.first(d).date,
to: _.last(d).date
})
});
}
Stitcher.prototype.seedLocalData = function(from, to, next) {
this.reader.get(from, to, 'full', function(err, rows) {
rows = _.map(rows, row => {
row.start = moment.unix(row.start);
return row;
});
this.batcher.write(rows);
this.reader.close();
next();
}.bind(this));
}
module.exports = Stitcher;