first commit

This commit is contained in:
Francois Klieber committed 2021-06-06 20:25:14 +02:00
commit aa7869eb33
386 files changed
+69375

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
// TODO: properly handle a daterange for which no data is available.
const batchSize = 1000;
const _ = require('lodash');
const fs = require('fs');
const moment = require('moment');
const util = require('../../core/util');
const config = util.getConfig();
const dirs = util.dirs();
const log = require(dirs.core + '/log');
const adapter = config[config.adapter];
const Reader = require(dirs.gekko + adapter.path + '/reader');
const daterange = config.daterange;
const CandleBatcher = require(dirs.core + 'candleBatcher');
const to = moment.utc(daterange.to).startOf('minute');
const from = moment.utc(daterange.from).startOf('minute');
const toUnix = to.unix();
if(to <= from)
util.die('This daterange does not make sense.')
if(!from.isValid())
util.die('invalid `from`');
if(!to.isValid())
util.die('invalid `to`');
let iterator = {
from: from.clone(),
to: from.clone().add(batchSize, 'm').subtract(1, 's')
}
var DONE = false;
var result = [];
var reader = new Reader();
var batcher;
var next;
var doneFn = () => {
process.nextTick(() => {
next(result);
})
};
module.exports = function(candleSize, _next) {
next = _.once(_next);
batcher = new CandleBatcher(candleSize)
.on('candle', handleBatchedCandles);
getBatch();
}
const getBatch = () => {
reader.get(
iterator.from.unix(),
iterator.to.unix(),
'full',
handleCandles
)
}
const shiftIterator = () => {
iterator = {
from: iterator.from.clone().add(batchSize, 'm'),
to: iterator.from.clone().add(batchSize * 2, 'm').subtract(1, 's')
}
}
const handleCandles = (err, data) => {
if(err) {
console.error(err);
util.die('Encountered an error..')
}
if(_.size(data) && _.last(data).start >= toUnix || iterator.from.unix() >= toUnix)
DONE = true;
batcher.write(data);
batcher.flush();
if(DONE) {
reader.close();
setTimeout(doneFn, 100);
} else {
shiftIterator();
getBatch();
}
}
const handleBatchedCandles = candle => {
result.push(candle);
}
+42
View File
@@ -0,0 +1,42 @@
const fs = require('fs');
const _ = require('lodash');
const toml = require('toml');
const util = require('../util');
const dirs = util.dirs();
const getTOML = function(fileName) {
var raw = fs.readFileSync(fileName);
return toml.parse(raw);
}
// build a config object out of a directory of TOML files
module.exports = function() {
const configDir = util.dirs().config;
let _config = getTOML(configDir + 'general.toml');
fs.readdirSync(configDir + 'plugins').forEach(function(pluginFile) {
let pluginName = _.first(pluginFile.split('.'))
_config[pluginName] = getTOML(configDir + 'plugins/' + pluginFile);
});
// attach the proper adapter
let adapter = _config.adapter;
_config[adapter] = getTOML(configDir + 'adapters/' + adapter + '.toml');
if(_config.tradingAdvisor.enabled) {
// also load the strat
let strat = _config.tradingAdvisor.method;
let stratFile = configDir + 'strategies/' + strat + '.toml';
if(!fs.existsSync(stratFile))
util.die('Cannot find the strategy config file for ' + strat);
_config[strat] = getTOML(stratFile);
}
const mode = util.gekkoMode();
if(mode === 'backtest')
_config.backtest = getTOML(configDir + 'backtest.toml');
return _config;
}
+233
View File
@@ -0,0 +1,233 @@
var _ = require('lodash');
var fs = require('fs');
var moment = require('moment');
var util = require('../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() {
require(dirs.gekko + 'exchange/dependencyCheck');
const exchangeChecker = require(dirs.gekko + 'exchange/exchangeChecker');
const slug = config.watch.exchange.toLowerCase();
let exchange;
try {
exchange = exchangeChecker.getExchangeCapabilities(slug);
} catch(e) {
util.die(e.message);
}
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(requiredHistory, 'm');
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 if (idealStartTime.unix() < localData.from) {
log.info('\tLocal data is still too recent, 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 Gekko 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 Gekko from requesting', minutesAgo, 'minutes of history.');
idealExchangeStartTime = endTime.clone().subtract(maxMinutesAgo, 'minutes');
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 (Gekko 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(1, 'minute')
.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().gekko + 'exchange/wrappers/' + 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(e) {
util.die(e.message);
}
if(_.isEmpty(d))
return util.die(
`Gekko 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.batcher.flush();
this.reader.close();
next();
}.bind(this));
}
module.exports = Stitcher;
+138
View File
@@ -0,0 +1,138 @@
var BATCH_SIZE = 60; // minutes
var MISSING_CANDLES_ALLOWED = 3; // minutes, per batch
var _ = require('lodash');
var moment = require('moment');
var async = require('async');
var util = require('../util');
var config = util.getConfig();
var dirs = util.dirs();
var log = require(dirs.core + 'log');
var adapter = config[config.adapter];
var Reader = require(dirs.gekko + adapter.path + '/reader');
var reader = new Reader();
// todo: rewrite with generators or async/await..
var scan = function(done) {
log.info('Scanning local history for backtestable dateranges.');
reader.tableExists('candles', (err, exists) => {
if(err)
return done(err, null, reader);
if(!exists)
return done(null, [], reader);
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.');
return done(false, [{
from: first,
to: last
}], reader);
}
// 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 + MISSING_CANDLES_ALLOWED > BATCH_SIZE;
if(complete)
batches.push({
to: to,
from: from
});
next();
}
);
iterator.from -= BATCH_SIZE * 60;
iterator.to -= BATCH_SIZE * 60;
},
() => {
if(batches.length === 0) {
return done(null, [], reader);
}
// 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.
return done(false, ranges, reader);
}
)
});
});
}
module.exports = scan;