first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,36 @@
|
||||
var log = require('../core/log');
|
||||
var moment = require('moment');
|
||||
var _ = require('lodash');
|
||||
var util = require('../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var adviceLoggerConfig = config.adviceLogger;
|
||||
|
||||
var Actor = function() {
|
||||
this.price = 'N/A';
|
||||
this.marketTime = {format: function() {return 'N/A'}};
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
Actor.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
this.marketTime = candle.start;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Actor.prototype.processAdvice = function(advice) {
|
||||
if (adviceLoggerConfig.muteSoft && advice.recommendation == 'soft') return;
|
||||
console.log()
|
||||
log.info('We have new trading advice!');
|
||||
log.info('\t Position:', advice.recommendation);
|
||||
log.info('\t Market price:', this.price);
|
||||
log.info('\t Based on market time:', this.marketTime.format('YYYY-MM-DD HH:mm:ss'));
|
||||
console.log()
|
||||
};
|
||||
|
||||
Actor.prototype.finalize = function(advice, done) {
|
||||
// todo
|
||||
done();
|
||||
};
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,146 @@
|
||||
// Small plugin that subscribes to some events, stores
|
||||
// them and sends it to the parent process.
|
||||
|
||||
const log = require('../core/log');
|
||||
const _ = require('lodash');
|
||||
const util = require('../core/util.js');
|
||||
const env = util.gekkoEnv();
|
||||
const config = util.getConfig();
|
||||
const moment = require('moment');
|
||||
const fs = require('fs');
|
||||
|
||||
const BacktestResultExporter = function() {
|
||||
this.performanceReport;
|
||||
this.roundtrips = [];
|
||||
this.stratUpdates = [];
|
||||
this.stratCandles = [];
|
||||
this.trades = [];
|
||||
|
||||
this.candleProps = config.backtestResultExporter.data.stratCandleProps;
|
||||
|
||||
if(!config.backtestResultExporter.data.stratUpdates)
|
||||
this.processStratUpdate = null;
|
||||
|
||||
if(!config.backtestResultExporter.data.roundtrips)
|
||||
this.processRoundtrip = null;
|
||||
|
||||
if(!config.backtestResultExporter.data.stratCandles)
|
||||
this.processStratCandles = null;
|
||||
|
||||
if(!config.backtestResultExporter.data.portfolioValues)
|
||||
this.processPortfolioValueChange = null;
|
||||
|
||||
if(!config.backtestResultExporter.data.trades)
|
||||
this.processTradeCompleted = null;
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
BacktestResultExporter.prototype.processPortfolioValueChange = function(portfolio) {
|
||||
this.portfolioValue = portfolio.balance;
|
||||
}
|
||||
|
||||
BacktestResultExporter.prototype.processStratCandle = function(candle) {
|
||||
let strippedCandle;
|
||||
|
||||
if(!this.candleProps) {
|
||||
strippedCandle = {
|
||||
...candle,
|
||||
start: candle.start.unix()
|
||||
}
|
||||
} else {
|
||||
strippedCandle = {
|
||||
..._.pick(candle, this.candleProps),
|
||||
start: candle.start.unix()
|
||||
}
|
||||
}
|
||||
|
||||
if(config.backtestResultExporter.data.portfolioValues)
|
||||
strippedCandle.portfolioValue = this.portfolioValue;
|
||||
|
||||
this.stratCandles.push(strippedCandle);
|
||||
};
|
||||
|
||||
BacktestResultExporter.prototype.processRoundtrip = function(roundtrip) {
|
||||
this.roundtrips.push({
|
||||
...roundtrip,
|
||||
entryAt: roundtrip.entryAt.unix(),
|
||||
exitAt: roundtrip.exitAt.unix()
|
||||
});
|
||||
};
|
||||
|
||||
BacktestResultExporter.prototype.processTradeCompleted = function(trade) {
|
||||
this.trades.push({
|
||||
...trade,
|
||||
date: trade.date.unix()
|
||||
});
|
||||
};
|
||||
|
||||
BacktestResultExporter.prototype.processStratUpdate = function(stratUpdate) {
|
||||
this.stratUpdates.push({
|
||||
...stratUpdate,
|
||||
date: stratUpdate.date.unix()
|
||||
});
|
||||
}
|
||||
|
||||
BacktestResultExporter.prototype.processPerformanceReport = function(performanceReport) {
|
||||
this.performanceReport = performanceReport;
|
||||
}
|
||||
|
||||
BacktestResultExporter.prototype.finalize = function(done) {
|
||||
const backtest = {
|
||||
market: config.watch,
|
||||
tradingAdvisor: config.tradingAdvisor,
|
||||
strategyParameters: config[config.tradingAdvisor.method],
|
||||
performanceReport: this.performanceReport
|
||||
};
|
||||
|
||||
if(config.backtestResultExporter.data.stratUpdates)
|
||||
backtest.stratUpdates = this.stratUpdates;
|
||||
|
||||
if(config.backtestResultExporter.data.roundtrips)
|
||||
backtest.roundtrips = this.roundtrips;
|
||||
|
||||
if(config.backtestResultExporter.data.stratCandles)
|
||||
backtest.stratCandles = this.stratCandles;
|
||||
|
||||
if(config.backtestResultExporter.data.trades)
|
||||
backtest.trades = this.trades;
|
||||
|
||||
if(env === 'child-process') {
|
||||
process.send({backtest});
|
||||
}
|
||||
|
||||
if(config.backtestResultExporter.writeToDisk) {
|
||||
this.writeToDisk(backtest, done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
BacktestResultExporter.prototype.writeToDisk = function(backtest, next) {
|
||||
let filename;
|
||||
|
||||
if(config.backtestResultExporter.filename) {
|
||||
filename = config.backtestResultExporter.filename;
|
||||
} else {
|
||||
const now = moment().format('YYYY-MM-DD_HH-mm-ss');
|
||||
filename = `backtest-${config.tradingAdvisor.method}-${now}.json`;
|
||||
}
|
||||
|
||||
fs.writeFile(
|
||||
util.dirs().gekko + filename,
|
||||
JSON.stringify(backtest),
|
||||
err => {
|
||||
if(err) {
|
||||
log.error('unable to write backtest result', err);
|
||||
} else {
|
||||
log.info('written backtest to: ', util.dirs().gekko + filename);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = BacktestResultExporter;
|
||||
@@ -0,0 +1,118 @@
|
||||
const fsw = require('fs');
|
||||
const _ = require('lodash');
|
||||
const log = require('../core/log.js');
|
||||
const util = require('../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const blotterConfig = config.blotter;
|
||||
|
||||
var Blotter = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.time;
|
||||
this.valueAtBuy = 0.0;
|
||||
this.filename = blotterConfig.filename;
|
||||
this.dateformat = blotterConfig.dateFormat;
|
||||
this.timezone = blotterConfig.timezone;
|
||||
this.headertxt = '';
|
||||
this.outtxt = '';
|
||||
this.tradeError = false;
|
||||
this.inaccurateData = false;
|
||||
|
||||
this.done = done;
|
||||
this.setup();
|
||||
};
|
||||
|
||||
Blotter.prototype.setup = function(done) {
|
||||
|
||||
this.headertxt = "Date,Price,Amount,Side,Fees,Value,P&L,Notes\n";
|
||||
|
||||
fsw.readFile(this.filename, (err, _) => {
|
||||
if (err) {
|
||||
log.warn('No file with the name', this.filename, 'found. Creating new blotter file');
|
||||
fsw.appendFile(this.filename, this.headertxt, 'utf8', (err) => {
|
||||
if(err) {
|
||||
log.error('Unable to write header text to blotter');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
Blotter.prototype.processTradeCompleted = function(trade) {
|
||||
// if exchange doesn't send correct timezone, correct it
|
||||
if (trade.date.format('Z') == '+00:00') {
|
||||
var adjustTimezone = trade.date.utcOffset(this.timezone);
|
||||
this.time = adjustTimezone.format(this.dateformat);
|
||||
} else {
|
||||
this.time = trade.date.format(this.dateformat);
|
||||
}
|
||||
// If a trade date is from 1969 or 1970, there was an error with the trade
|
||||
if (trade.date.format('YY') == '69' || trade.date.format('YY') == '70') {
|
||||
log.error('Received 1969/1970 error, trade failed to execute, did not record in blotter');
|
||||
// Prevent roundTrip from writing error P&L in processRoundtrip method
|
||||
if (trade.action == 'sell') {
|
||||
this.tradeError = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.valueAtBuy = this.roundUp(trade.effectivePrice * trade.amount);
|
||||
|
||||
if (trade.action === 'buy') {
|
||||
//time, price, amount, side, fees, value at buy
|
||||
this.outtxt = this.time + "," + trade.effectivePrice.toFixed(2) + "," + trade.amount.toFixed(8) + "," + trade.action + "," + trade.feePercent + "," + this.valueAtBuy;
|
||||
if ((trade.price == 0 || isNaN(trade.price)) && (trade.amount == 0|| isNaN(trade.amount))) {
|
||||
this.outtxt = this.outtxt + "," + ",Trade probably went through but didn't receive correct price/amount info\n";
|
||||
} else {
|
||||
this.outtxt = this.outtxt + "\n";
|
||||
}
|
||||
this.writeBlotter();
|
||||
return;
|
||||
}
|
||||
else if (trade.action === 'sell'){
|
||||
var sellValue = (this.roundUp(trade.effectivePrice * trade.amount));
|
||||
//time, price, amount, side, fees, value at sell
|
||||
this.outtxt = this.time + "," + trade.effectivePrice.toFixed(2) + "," + trade.amount.toFixed(8) + "," + trade.action + "," + trade.feePercent + "," + sellValue + ",";
|
||||
if ((trade.price == 0 || isNaN(trade.price)) && (trade.amount == 0|| isNaN(trade.amount))) {
|
||||
this.inaccurateData = true;
|
||||
}
|
||||
this.valueAtBuy = 0.0;
|
||||
// wait til processRoundtrip complete to write to file
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Blotter.prototype.writeBlotter = function() {
|
||||
fsw.appendFile(this.filename, this.outtxt, 'utf8', (err) => {
|
||||
if(err) {
|
||||
log.error('Unable to write trade to blotter');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Blotter.prototype.processRoundtrip = function(trip) {
|
||||
log.info('Roundtrip', trip);
|
||||
if (!this.tradeError) {
|
||||
this.outtxt = this.outtxt + this.roundUp(trip.pnl) +'\n';
|
||||
this.writeBlotter();
|
||||
return;
|
||||
}
|
||||
if (this.inaccurateData) {
|
||||
this.outtxt = this.outtxt + this.roundUp(trip.pnl) + ",Trade probably went through but didn't receive correct price/amount info\n";
|
||||
this.inaccurateData = false;
|
||||
this.writeBlotter;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1969/1970 sell trade error not written to blotter, resetting flag to false
|
||||
this.tradeError = false;
|
||||
}
|
||||
|
||||
Blotter.prototype.roundUp = function(value) {
|
||||
var cents = value * 100;
|
||||
var roundedCents = Math.round(cents);
|
||||
return roundedCents / 100;
|
||||
}
|
||||
|
||||
module.exports = Blotter;
|
||||
@@ -0,0 +1,132 @@
|
||||
var _ = require('lodash');
|
||||
var Moment = require('moment');
|
||||
var Ranger = require('ranger');
|
||||
|
||||
var config = require('../core/util').getConfig().campfire;
|
||||
|
||||
var Actor = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
this.commands = [{
|
||||
'handler': 'advice',
|
||||
'callback': this.sayAdvice,
|
||||
'description': "Advice on what position to take, depending on the current trend"
|
||||
}, {
|
||||
'handler': 'price',
|
||||
'callback': this.sayPrice,
|
||||
'description': "The current price of the asset in the configured currency"
|
||||
}, {
|
||||
'handler': 'donate',
|
||||
'callback': this.sayDonate,
|
||||
'description': "Where to send all of that extra coin that's weighing you down"
|
||||
}, {
|
||||
'handler': 'help',
|
||||
'callback': this.sayHelp,
|
||||
'description': "You are here"
|
||||
}];
|
||||
|
||||
this.advice = null;
|
||||
this.adviceTime = Moment.utc();
|
||||
this.price = null;
|
||||
this.priceTime = Moment.utc();
|
||||
|
||||
this.client = Ranger.createClient(config.account, config.apiKey);
|
||||
this.client.room(config.roomId, this.joinRoom);
|
||||
this.client.me(this.whoAmI);
|
||||
};
|
||||
|
||||
Actor.prototype = {
|
||||
processCandle: function(candle, done) {
|
||||
this.price = candle.close;
|
||||
this.priceTime = candle.date.start();
|
||||
|
||||
done();
|
||||
},
|
||||
|
||||
processAdvice: function(advice) {
|
||||
if (campfire.muteSoft && advice.recommendation === 'soft') return;
|
||||
this.advice = advice.recommendation;
|
||||
this.adviceTime = Moment.utc();
|
||||
|
||||
if (config.emitUpdates) {
|
||||
this.sayAdvice();
|
||||
}
|
||||
},
|
||||
|
||||
sayAdvice: function() {
|
||||
var message;
|
||||
|
||||
if (this.advice !== null) {
|
||||
message = ["We think you should", this.advice + ".", "(" + this.adviceTime.fromNow() + ")"];
|
||||
} else {
|
||||
message = ["We don't have any advice for you quite yet."];
|
||||
}
|
||||
|
||||
this.room.speak(message.join(' '));
|
||||
},
|
||||
|
||||
sayPrice: function() {
|
||||
var message;
|
||||
|
||||
if (this.price !== null) {
|
||||
message = ["The price at the moment is", this.price + ".", "(" + this.priceTime.fromNow() + ")"];
|
||||
} else {
|
||||
message = ["We don't know the price right now."];
|
||||
}
|
||||
|
||||
this.room.speak(message.join(' '));
|
||||
},
|
||||
|
||||
sayDonate: function() {
|
||||
this.room.speak("If you'd like to donate a few coins, you can send them here: 13r1jyivitShUiv9FJvjLH7Nh1ZZptumwW");
|
||||
},
|
||||
|
||||
sayHelp: function() {
|
||||
this.room.speak("I listen for the following inquiries...", this.pasteDescriptions);
|
||||
},
|
||||
|
||||
pasteDescriptions: function() {
|
||||
var descriptions = _.map(this.commands, function(command) {
|
||||
return [command.handler + ':', command.description].join(' ');
|
||||
}, this).join('\n');
|
||||
|
||||
this.room.paste(descriptions);
|
||||
},
|
||||
|
||||
joinRoom: function(room) {
|
||||
this.room = room;
|
||||
this.room.join(this.listenForCommands);
|
||||
},
|
||||
|
||||
listenForCommands: function() {
|
||||
this.room.listen(this.executeCommands);
|
||||
},
|
||||
|
||||
executeCommands: function(message) {
|
||||
if (message.userId === this.user.id) return false; // Make the bot ignore itself
|
||||
if (message.body === null) return false; // Handle weird cases where body is null sometimes
|
||||
|
||||
_.each(this.commands, function(command) {
|
||||
if (this.textHasCommandForBot(message.body, config.nickname, command.handler)) {
|
||||
command.callback();
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
textHasCommandForBot: function(text, nickname, handler) {
|
||||
var normalizedText = text.toLowerCase(),
|
||||
normalizedNickname = nickname.toLowerCase(),
|
||||
normalizedHandler = handler.toLowerCase();
|
||||
|
||||
var nicknameWasMentioned = normalizedText.indexOf(normalizedNickname) >= 0,
|
||||
handlerWasMentioned = normalizedText.indexOf(normalizedHandler) >= 0;
|
||||
|
||||
return nicknameWasMentioned && handlerWasMentioned;
|
||||
},
|
||||
|
||||
whoAmI: function(user) {
|
||||
this.user = user;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,77 @@
|
||||
const axios = require('axios');
|
||||
const _ = require('lodash');
|
||||
const log = require('../core/log.js');
|
||||
const util = require('../core/util.js');
|
||||
const config = util.getConfig();
|
||||
|
||||
const CandleUploader = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
done();
|
||||
this.candles = [];
|
||||
this.schedule();
|
||||
};
|
||||
|
||||
CandleUploader.prototype.processCandle = function(candle, done) {
|
||||
this.candles.push(candle);
|
||||
done();
|
||||
};
|
||||
|
||||
CandleUploader.prototype.schedule = function() {
|
||||
this.timer = setTimeout(this.upload, 10 * 1000);
|
||||
}
|
||||
|
||||
CandleUploader.prototype.rawUpload = function(candles, count, next) {
|
||||
|
||||
const amount = candles.length;
|
||||
|
||||
axios({
|
||||
url: config.candleUploader.url,
|
||||
method: 'post',
|
||||
data: {
|
||||
apiKey: config.candleUploader.apiKey,
|
||||
watch: config.watch,
|
||||
candles: candles
|
||||
}
|
||||
})
|
||||
.then(r => {
|
||||
if(r.data.success === false) {
|
||||
console.log('error uploading:', r.data);
|
||||
}
|
||||
console.log(new Date, 'uploaded', amount, 'candles');
|
||||
|
||||
next();
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('error uploading:', e.message);
|
||||
|
||||
count++;
|
||||
|
||||
if(count > 10) {
|
||||
console.log('FINAL error uploading:', e.message);
|
||||
return next();
|
||||
}
|
||||
|
||||
setTimeout(() => this.rawUpload(candles, count, next), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
CandleUploader.prototype.upload = function() {
|
||||
const amount = this.candles.length;
|
||||
if(!amount) {
|
||||
return this.schedule();
|
||||
}
|
||||
|
||||
this.rawUpload(this.candles, 0, () => {
|
||||
this.schedule();
|
||||
});
|
||||
|
||||
this.candles = [];
|
||||
}
|
||||
|
||||
CandleUploader.prototype.finish = function(next) {
|
||||
this.upload();
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
module.exports = CandleUploader;
|
||||
@@ -0,0 +1,24 @@
|
||||
// Small plugin that subscribes to some events, stores
|
||||
// them and sends it to the parent process.
|
||||
|
||||
const log = require('../core/log');
|
||||
const _ = require('lodash');
|
||||
const subscriptions = require('../subscriptions');
|
||||
const config = require('../core/util').getConfig();
|
||||
|
||||
const ChildToParent = function() {
|
||||
|
||||
subscriptions
|
||||
// .filter(sub => config.childToParent.events.includes(sub.event))
|
||||
.forEach(sub => {
|
||||
this[sub.handler] = (event, next) => {
|
||||
process.send({type: sub.event, payload: event});
|
||||
if(_.isFunction(next)) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
}
|
||||
|
||||
module.exports = ChildToParent;
|
||||
@@ -0,0 +1,20 @@
|
||||
const log = require('../core/log');
|
||||
const _ = require('lodash');
|
||||
const subscriptions = require('../subscriptions');
|
||||
const config = require('../core/util').getConfig().eventLogger;
|
||||
|
||||
const EventLogger = function() {}
|
||||
|
||||
_.each(subscriptions, sub => {
|
||||
if(config.whitelist && !config.whitelist.includes(sub.event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventLogger.prototype[sub.handler] = (event, next) => {
|
||||
log.info(`\t\t\t\t[EVENT ${sub.event}]\n`, event);
|
||||
if(_.isFunction(next))
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = EventLogger;
|
||||
@@ -0,0 +1,88 @@
|
||||
const superagent = require('superagent');
|
||||
const _ = require('lodash');
|
||||
const log = require('../core/log.js');
|
||||
const util = require('../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const iftttConfig = config.ifttt;
|
||||
|
||||
const IFTTT = function(done) {
|
||||
_.bindAll(this);
|
||||
this.ifttt;
|
||||
this.price = 'N/A';
|
||||
this.done = done;
|
||||
this.setup();
|
||||
};
|
||||
|
||||
IFTTT.prototype.setup = function(done) {
|
||||
var setupIFTTT = function () {
|
||||
if(iftttConfig.sendMessageOnStart){
|
||||
var exchange = config.watch.exchange;
|
||||
var currency = config.watch.currency;
|
||||
var asset = config.watch.asset;
|
||||
var body = "Gekko has started, Ive started watching "
|
||||
+exchange
|
||||
+" "
|
||||
+currency
|
||||
+" "
|
||||
+asset
|
||||
+" I'll let you know when I got some advice";
|
||||
this.send(body);
|
||||
}else{
|
||||
log.debug('Skipping Send message on startup')
|
||||
}
|
||||
};
|
||||
setupIFTTT.call(this)
|
||||
};
|
||||
|
||||
IFTTT.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
IFTTT.prototype.portfolioUpdate = function(portfolio) {
|
||||
var message = "Gekko has detected a portfolio update. " +
|
||||
"Your current " + config.watch.currency + " balance is " + portfolio.currency + '.' +
|
||||
"Your current " + config.watch.exchange + " balance is " + portfolio.assert + '.';
|
||||
this.send(message);
|
||||
}
|
||||
|
||||
IFTTT.prototype.processAdvice = function(advice) {
|
||||
if (advice.recommendation == 'soft' && iftttConfig.muteSoft) return;
|
||||
|
||||
const text = [
|
||||
'Gekko is watching ',
|
||||
config.watch.exchange,
|
||||
' and has detected a new trend, advice is to go ',
|
||||
advice.recommendation,
|
||||
'.\n\nThe current ',
|
||||
config.watch.asset,
|
||||
' price is ',
|
||||
this.price
|
||||
].join('');
|
||||
|
||||
this.send(text);
|
||||
};
|
||||
|
||||
IFTTT.prototype.send = function(content) {
|
||||
superagent.
|
||||
post('https://maker.ifttt.com/trigger/' + iftttConfig.eventName + '/with/key/' + iftttConfig.makerKey)
|
||||
.send({value1: content})
|
||||
.end(function(err, res){
|
||||
if(err || !res){
|
||||
log.error('IFTTT ERROR:', error)
|
||||
}else{
|
||||
log.info('IFTTT Message Sent')
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
IFTTT.prototype.checkResults = function(error) {
|
||||
if (error) {
|
||||
log.warn('error sending IFTTT', error);
|
||||
} else {
|
||||
log.info('Send advice via IFTTT.');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = IFTTT;
|
||||
@@ -0,0 +1,154 @@
|
||||
var log = require('../core/log');
|
||||
var moment = require('moment');
|
||||
var _ = require('lodash');
|
||||
var config = require('../core/util').getConfig();
|
||||
var ircbot = config.ircbot;
|
||||
var utc = moment.utc;
|
||||
|
||||
var irc = require("irc");
|
||||
|
||||
var Actor = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
this.bot = new irc.Client(ircbot.server, ircbot.botName, {
|
||||
channels: [ ircbot.channel ]
|
||||
});
|
||||
|
||||
this.bot.addListener("message", this.verifyQuestion);
|
||||
this.bot.addListener("error", this.logError);
|
||||
|
||||
this.advice = 'Dont got one yet :(';
|
||||
this.adviceTime = utc();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Actor.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
this.priceTime = candle.start;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Actor.prototype.processAdvice = function(advice) {
|
||||
if (advice.recommendation == "soft" && ircbot.muteSoft) return;
|
||||
this.advice = advice.recommendation;
|
||||
this.adviceTime = utc();
|
||||
|
||||
if(ircbot.emitUpdates)
|
||||
this.newAdvice();
|
||||
};
|
||||
|
||||
Actor.prototype.verifyQuestion = function(from, to, text, message) {
|
||||
if(text in this.commands)
|
||||
this[this.commands[text]]();
|
||||
}
|
||||
|
||||
Actor.prototype.newAdvice = function() {
|
||||
this.bot.say(ircbot.channel, 'Guys! Important news!');
|
||||
this.emitAdvice();
|
||||
}
|
||||
|
||||
// sent advice over to the IRC channel
|
||||
Actor.prototype.emitAdvice = function() {
|
||||
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.bot.say(ircbot.channel, message);
|
||||
};
|
||||
|
||||
// sent price over to the IRC channel
|
||||
Actor.prototype.emitPrice = function() {
|
||||
|
||||
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.bot.say(ircbot.channel, message);
|
||||
};
|
||||
|
||||
// sent donation info over to the IRC channel
|
||||
Actor.prototype.emitDonation = function() {
|
||||
var message = 'You want to donate? How nice of you! You can send your coins here:';
|
||||
message += '\nBTC:\t13r1jyivitShUiv9FJvjLH7Nh1ZZptumwW';
|
||||
|
||||
this.bot.say(ircbot.channel, message);
|
||||
};
|
||||
|
||||
Actor.prototype.emitHelp = function() {
|
||||
var message = _.reduce(
|
||||
this.rawCommands,
|
||||
function(message, command) {
|
||||
return message + ' ' + command + ',';
|
||||
},
|
||||
'possible commands are:'
|
||||
);
|
||||
|
||||
message = message.substr(0, _.size(message) - 1) + '.';
|
||||
|
||||
this.bot.say(ircbot.channel, message);
|
||||
|
||||
}
|
||||
|
||||
Actor.prototype.emitRealAdvice = function() {
|
||||
// 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.bot.say(ircbot.channel, _.first(_.shuffle(realAdvice)));
|
||||
}
|
||||
|
||||
Actor.prototype.logError = function(message) {
|
||||
log.error('IRC ERROR:', message);
|
||||
};
|
||||
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Created by billymcintosh on 24/12/17.
|
||||
*/
|
||||
|
||||
var _ = require('lodash');
|
||||
var request = require('request');
|
||||
var log = require('../core/log.js');
|
||||
var util = require('../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var kodiConfig = config.kodi;
|
||||
|
||||
var Kodi = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.exchange = config.watch.exchange.charAt().toUpperCase() + config.watch.exchange.slice(1)
|
||||
|
||||
this.price = 'N/A';
|
||||
this.done = done;
|
||||
this.setup();
|
||||
};
|
||||
|
||||
Kodi.prototype.setup = function(done) {
|
||||
var setupKodi = function (err, result) {
|
||||
if(kodiConfig.sendMessageOnStart) {
|
||||
var currency = config.watch.currency;
|
||||
var asset = config.watch.asset;
|
||||
var title = "Gekko Started";
|
||||
var message = `Watching ${this.exchange} - ${currency}/${asset}`;
|
||||
this.mail(title, message);
|
||||
} else {
|
||||
log.debug('Skipping Send message on startup')
|
||||
}
|
||||
}
|
||||
setupKodi.call(this)
|
||||
};
|
||||
|
||||
Kodi.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Kodi.prototype.processAdvice = function(advice) {
|
||||
var title = `Gekko: Going ${advice.recommendation} @ ${this.price}`
|
||||
var message = `${this.exchange} ${config.watch.currency}/${config.watch.asset}`;
|
||||
this.mail(title, message);
|
||||
};
|
||||
|
||||
Kodi.prototype.mail = function(title, message, done) {
|
||||
var options = {
|
||||
body: `{"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"${title}","message":"${message}"},"id":1}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
url: kodiConfig.host
|
||||
}
|
||||
|
||||
request(options, (error, response, body) => {
|
||||
if (!error) {
|
||||
log.info('Kodi message sent')
|
||||
} else {
|
||||
log.debug(`Kodi ${error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = Kodi;
|
||||
@@ -0,0 +1,138 @@
|
||||
var email = require("emailjs");
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log.js');
|
||||
var util = require('../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var mailConfig = config.mailer;
|
||||
|
||||
var Mailer = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.server;
|
||||
this.price = 'N/A';
|
||||
|
||||
this.done = done;
|
||||
this.setup();
|
||||
};
|
||||
|
||||
Mailer.prototype.setup = function(done) {
|
||||
var setupMail = function(err, result) {
|
||||
if(result) {
|
||||
console.log('Got it.');
|
||||
mailConfig.password = result.password;
|
||||
}
|
||||
|
||||
if(_.isEmpty(mailConfig.to))
|
||||
mailConfig.to = mailConfig.email;
|
||||
if(_.isEmpty(mailConfig.from))
|
||||
mailConfig.from = mailConfig.email;
|
||||
if(_.isEmpty(mailConfig.user) && mailConfig.smtpauth)
|
||||
mailConfig.user = mailConfig.email;
|
||||
|
||||
this.server = email.server.connect({
|
||||
user: mailConfig.user,
|
||||
password: mailConfig.password,
|
||||
host: mailConfig.server,
|
||||
ssl: mailConfig.ssl,
|
||||
port: mailConfig.port,
|
||||
tls: mailConfig.tls
|
||||
});
|
||||
|
||||
if(mailConfig.sendMailOnStart) {
|
||||
this.mail(
|
||||
"Gekko has started",
|
||||
[
|
||||
"I've just started watching ",
|
||||
config.watch.exchange,
|
||||
' ',
|
||||
config.watch.currency,
|
||||
'/',
|
||||
config.watch.asset,
|
||||
". I'll let you know when I got some advice"
|
||||
].join(''),
|
||||
_.bind(function(err) {
|
||||
this.checkResults(err);
|
||||
this.done();
|
||||
}, this)
|
||||
);
|
||||
} else
|
||||
this.done();
|
||||
|
||||
log.debug('Setup email adviser.');
|
||||
};
|
||||
|
||||
if(!mailConfig.password) {
|
||||
// ask for the mail password
|
||||
var prompt = require('prompt-lite');
|
||||
prompt.start();
|
||||
var warning = [
|
||||
'\n\n\tYou configured Gekko to mail you advice, Gekko needs your email',
|
||||
'password to send emails (to you). Gekko is an opensource project',
|
||||
'[ http://github.com/askmike/gekko ], you can take my word but always',
|
||||
'check the code yourself.',
|
||||
'\n\n\tWARNING: If you have not downloaded Gekko from the github page above we',
|
||||
'CANNOT guarantuee that your email address & password are safe!\n'
|
||||
].join('\n\t');
|
||||
log.warn(warning);
|
||||
prompt.get({name: 'password', hidden: true}, _.bind(setupMail, this));
|
||||
} else {
|
||||
setupMail.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
Mailer.prototype.mail = function(subject, content, done) {
|
||||
|
||||
this.server.send({
|
||||
text: content,
|
||||
from: mailConfig.from,
|
||||
to: mailConfig.to,
|
||||
subject: mailConfig.tag + subject
|
||||
}, done || this.checkResults);
|
||||
};
|
||||
|
||||
Mailer.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Mailer.prototype.processAdvice = function(advice) {
|
||||
|
||||
if (advice.recommendation == "soft" && mailConfig.muteSoft) return;
|
||||
|
||||
var text = [
|
||||
'Gekko is watching ',
|
||||
config.watch.exchange,
|
||||
' and has detected a new trend, advice is to go ',
|
||||
advice.recommendation,
|
||||
'.\n\nThe current ',
|
||||
config.watch.asset,
|
||||
' price is ',
|
||||
config.watch.currency,
|
||||
' ',
|
||||
this.price
|
||||
].join('');
|
||||
|
||||
var subject = 'New advice: go ' + advice.recommendation;
|
||||
|
||||
this.mail(subject, text);
|
||||
};
|
||||
|
||||
Mailer.prototype.processStratNotification = function({ content }) {
|
||||
const subject = `New notification from ${config.tradingAdvisor.method}`;
|
||||
const text = [
|
||||
'Gekko received new notification :\n\n',
|
||||
content
|
||||
].join('');
|
||||
|
||||
this.mail(subject, text);
|
||||
}
|
||||
|
||||
Mailer.prototype.checkResults = function(err) {
|
||||
if(err)
|
||||
log.warn('error sending email', err);
|
||||
else
|
||||
log.info('Send advice via email.');
|
||||
};
|
||||
|
||||
module.exports = Mailer;
|
||||
@@ -0,0 +1,47 @@
|
||||
var mongojs = require('mongojs');
|
||||
var mongoUtil = require('./util');
|
||||
|
||||
var util = require('../../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var dirs = util.dirs();
|
||||
|
||||
// verify the correct dependencies are installed
|
||||
var pluginHelper = require(`${dirs.core}pluginUtil`);
|
||||
var pluginMock = {
|
||||
slug: 'mongodb adapter',
|
||||
dependencies: config.mongodb.dependencies
|
||||
}
|
||||
|
||||
// exit if plugin couldn't be loaded
|
||||
var cannotLoad = pluginHelper.cannotLoad(pluginMock);
|
||||
if (cannotLoad) {
|
||||
util.die(cannotLoad);
|
||||
}
|
||||
|
||||
var mode = util.gekkoMode();
|
||||
|
||||
var collections = [
|
||||
mongoUtil.settings.historyCollection,
|
||||
mongoUtil.settings.adviceCollection
|
||||
]
|
||||
|
||||
var connection = mongojs(config.mongodb.connectionString, collections);
|
||||
var collection = connection.collection(mongoUtil.settings.historyCollection);
|
||||
|
||||
if (mode === 'backtest') {
|
||||
var pair = mongoUtil.settings.pair.join('_');
|
||||
|
||||
collection.find({ pair }).toArray((err, docs) => { // check if we've got any records
|
||||
if (err) {
|
||||
util.die(err);
|
||||
}
|
||||
if (docs.length === 0) {
|
||||
util.die(`History table for ${config.watch.exchange} with pair ${pair} is empty.`);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if(mongoUtil.settings.exchange) {
|
||||
collection.createIndex({start: 1, pair: 1}, {unique: true}); // create unique index on "time" and "pair"
|
||||
}
|
||||
module.exports = connection;
|
||||
@@ -0,0 +1,120 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('../../core/util.js');
|
||||
var log = require(`${util.dirs().core}log`);
|
||||
|
||||
var handle = require('./handle');
|
||||
var mongoUtil = require('./util');
|
||||
|
||||
var Reader = function Reader () {
|
||||
_.bindAll(this);
|
||||
this.db = handle;
|
||||
this.collection = this.db.collection(mongoUtil.settings.historyCollection);
|
||||
this.pair = mongoUtil.settings.pair.join('_');
|
||||
}
|
||||
|
||||
// returns the furtherst point (up to `from`) in time we have valid data from
|
||||
Reader.prototype.mostRecentWindow = function mostRecentWindow (from, to, next) {
|
||||
var mFrom = from.unix();
|
||||
var mTo = to.unix();
|
||||
|
||||
var maxAmount = mTo - mFrom + 1;
|
||||
|
||||
// Find some documents
|
||||
this.collection.find({ pair: this.pair, start: { $gte: mFrom, $lte: mTo } }).sort({ start: -1 }, (err, docs) => {
|
||||
if (err) {
|
||||
return util.die('DB error at `mostRecentWindow`');
|
||||
}
|
||||
// no candles are available
|
||||
if (docs.length === 0) {
|
||||
log.debug('no candles are available');
|
||||
return next(false);
|
||||
}
|
||||
|
||||
if (docs.length === maxAmount) {
|
||||
// full history is available!
|
||||
log.debug('full history is available!');
|
||||
return next({
|
||||
mFrom,
|
||||
mTo
|
||||
});
|
||||
}
|
||||
|
||||
// we have at least one gap, figure out where
|
||||
var mostRecent = _.first(docs).start;
|
||||
|
||||
var gapIndex = _.findIndex(docs, (r, i) => r.start !== mostRecent - i * 60);
|
||||
|
||||
// if there was no gap in the records, but
|
||||
// there were not enough records.
|
||||
if (gapIndex === -1) {
|
||||
var leastRecent = _.last(docs).start;
|
||||
return next({
|
||||
from: leastRecent,
|
||||
to: mostRecent
|
||||
});
|
||||
}
|
||||
|
||||
// else return mostRecent and the
|
||||
// the minute before the gap
|
||||
return next({
|
||||
from: docs[gapIndex - 1].start,
|
||||
to: mostRecent
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
Reader.prototype.get = function get (from, to, what, next) { // returns all fields in general
|
||||
// Find some documents
|
||||
this.collection.find({ pair: this.pair, start: { $gte: from, $lte: to } }).sort({ start: 1 }, (err, docs) => {
|
||||
if (err) {
|
||||
return util.die('DB error at `get`');
|
||||
}
|
||||
return next(null, docs);
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.count = function fCount (from, to, next) {
|
||||
this.collection.count({ pair: this.pair, start: { $gte: from, $lte: to } }, (err, count) => {
|
||||
if (err) {
|
||||
return util.die('DB error at `count`');
|
||||
}
|
||||
return next(null, count);
|
||||
})
|
||||
}
|
||||
|
||||
Reader.prototype.countTotal = function countTotal (next) {
|
||||
this.collection.count({ pair: this.pair }, (err, count) => {
|
||||
if (err) {
|
||||
return util.die('DB error at `countTotal`');
|
||||
}
|
||||
return next(null, count);
|
||||
})
|
||||
}
|
||||
|
||||
Reader.prototype.getBoundry = function getBoundry (next) {
|
||||
this.collection.find({ pair: this.pair }, { start: 1 }).sort({ start: 1 }).limit(1, (err, docs) => {
|
||||
if (err) {
|
||||
return util.die('DB error at `getBoundry`');
|
||||
}
|
||||
var start = _.first(docs).start;
|
||||
|
||||
this.collection.find({ pair: this.pair }, { start: 1 }).sort({ start: -1 }).limit(1, (err2, docs2) => {
|
||||
if (err2) {
|
||||
return util.die('DB error at `getBoundry`');
|
||||
}
|
||||
var end = _.first(docs2).start;
|
||||
return next(null, { first: start, last: end });
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.tableExists = function(name, next) {
|
||||
return next(null, true); // Return true for backtest
|
||||
}
|
||||
|
||||
Reader.prototype.close = function close () {
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
module.exports = Reader;
|
||||
@@ -0,0 +1,40 @@
|
||||
const async = require('async');
|
||||
var _ = require('lodash');
|
||||
var util = require('../../core/util.js');
|
||||
var log = require(`${util.dirs().core}log`);
|
||||
|
||||
var handle = require('./handle');
|
||||
|
||||
module.exports = done => {
|
||||
this.db = handle;
|
||||
|
||||
let markets = [];
|
||||
async.waterfall([
|
||||
(cb) => {
|
||||
handle.getCollectionNames(cb)
|
||||
},
|
||||
(collections, cb) => {
|
||||
async.each(collections, (collection, cb) => {
|
||||
let [exchange, type] = collection.split('_');
|
||||
if (type === 'candles') {
|
||||
handle.collection(collection).distinct('pair', {}, (err, pairs) => {
|
||||
console.log(exchange);
|
||||
pairs.forEach((pair) => {
|
||||
pair = pair.split('_');
|
||||
markets.push({
|
||||
exchange: exchange,
|
||||
currency: _.first(pair),
|
||||
asset: _.last(pair)
|
||||
});
|
||||
});
|
||||
cb();
|
||||
})
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}, () => {
|
||||
cb(null, markets)
|
||||
})
|
||||
}
|
||||
], done)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
var config = require('../../core/util.js').getConfig();
|
||||
|
||||
var watch = config.watch;
|
||||
var exchangeLowerCase = watch ? watch.exchange.toLowerCase() : watch = {}; // Do not crash on this, not needed to read from db
|
||||
|
||||
var settings = {
|
||||
exchange: watch.exchange,
|
||||
pair: [watch.currency, watch.asset],
|
||||
historyCollection: `${exchangeLowerCase}_candles`,
|
||||
adviceCollection: `${exchangeLowerCase}_advices`
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
settings
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
var _ = require('lodash');
|
||||
var config = require('../../core/util.js').getConfig();
|
||||
|
||||
var moment = require('moment');
|
||||
var util = require('../../core/util.js');
|
||||
var log = require(`${util.dirs().core}log`)
|
||||
|
||||
var handle = require('./handle');
|
||||
var mongoUtil = require('./util');
|
||||
|
||||
var Store = function Store (done) {
|
||||
_.bindAll(this);
|
||||
this.done = done;
|
||||
this.db = handle;
|
||||
this.historyCollection = this.db.collection(mongoUtil.settings.historyCollection);
|
||||
this.adviceCollection = this.db.collection(mongoUtil.settings.adviceCollection);
|
||||
|
||||
this.candleCache = [];
|
||||
|
||||
this.pair = mongoUtil.settings.pair.join('_');
|
||||
|
||||
this.price = 'N/A';
|
||||
this.marketTime = 'N/A';
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
Store.prototype.writeCandles = function writeCandles () {
|
||||
if (_.isEmpty(this.candleCache)) { // nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
var candles = [];
|
||||
_.each(this.candleCache, candle => {
|
||||
var mCandle = {
|
||||
time: moment().unix(),
|
||||
start: candle.start.unix(),
|
||||
open: candle.open,
|
||||
high: candle.high,
|
||||
low: candle.low,
|
||||
close: candle.close,
|
||||
vwp: candle.vwp,
|
||||
volume: candle.volume,
|
||||
trades: candle.trades,
|
||||
pair: this.pair
|
||||
};
|
||||
candles.push(mCandle);
|
||||
});
|
||||
|
||||
// Fix error when whole batch would failed to insert if one or more duplicate candle
|
||||
this.historyCollection.insert(candles, { ordered: false }, e => {
|
||||
if (e) {
|
||||
let msg = 'mongojs insert() ' + e.writeErrors.length + ' of ' + candles.length + ' failed.';
|
||||
_.forEach(_.countBy(e.writeErrors, 'code'), (c, k) => {
|
||||
msg += ' Code: E' + k + ' count: ' + c;
|
||||
});
|
||||
log.debug(msg);
|
||||
}
|
||||
});
|
||||
|
||||
this.candleCache = [];
|
||||
}
|
||||
|
||||
var processCandle = function processCandle (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.price = candle.close; // used in adviceWriter
|
||||
this.marketTime = candle.start;
|
||||
|
||||
this.candleCache.push(candle);
|
||||
if (this.candleCache.length >= 100)
|
||||
this.writeCandles();
|
||||
done();
|
||||
}
|
||||
|
||||
var finalize = function(done) {
|
||||
this.writeCandles();
|
||||
// Fix connection closed before all candles was written to db
|
||||
setTimeout( () => {
|
||||
this.db = null;
|
||||
done();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
var processAdvice = function processAdvice (advice) {
|
||||
if (config.candleWriter.muteSoft && advice.recommendation === 'soft') {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Writing advice '${advice.recommendation}' to database.`);
|
||||
var mAdvice = {
|
||||
time: moment().unix(),
|
||||
marketTime: this.marketTime,
|
||||
pair: this.pair,
|
||||
recommendation: advice.recommendation,
|
||||
price: this.price,
|
||||
portfolio: advice.portfolio
|
||||
};
|
||||
|
||||
this.adviceCollection.insert(mAdvice);
|
||||
}
|
||||
|
||||
if (config.adviceWriter.enabled) {
|
||||
log.debug('Enabling adviceWriter.');
|
||||
Store.prototype.processAdvice = processAdvice;
|
||||
}
|
||||
|
||||
if (config.candleWriter.enabled) {
|
||||
log.debug('Enabling candleWriter.');
|
||||
Store.prototype.processCandle = processCandle;
|
||||
Store.prototype.finalize = finalize;
|
||||
}
|
||||
|
||||
module.exports = Store;
|
||||
@@ -0,0 +1,283 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const util = require('../../core/util');
|
||||
const ENV = util.gekkoEnv();
|
||||
|
||||
const config = util.getConfig();
|
||||
const calcConfig = config.paperTrader;
|
||||
const watchConfig = config.watch;
|
||||
const dirs = util.dirs();
|
||||
const log = require(dirs.core + 'log');
|
||||
|
||||
const TrailingStop = require(dirs.broker + 'triggers/trailingStop');
|
||||
|
||||
const PaperTrader = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
if(calcConfig.feeUsing === 'maker') {
|
||||
this.rawFee = calcConfig.feeMaker;
|
||||
} else {
|
||||
this.rawFee = calcConfig.feeTaker;
|
||||
}
|
||||
|
||||
this.fee = 1 - this.rawFee / 100;
|
||||
|
||||
this.currency = watchConfig.currency;
|
||||
this.asset = watchConfig.asset;
|
||||
|
||||
this.portfolio = {
|
||||
asset: calcConfig.simulationBalance.asset,
|
||||
currency: calcConfig.simulationBalance.currency,
|
||||
}
|
||||
|
||||
this.balance = false;
|
||||
|
||||
if(this.portfolio.asset > 0) {
|
||||
this.exposed = true;
|
||||
}
|
||||
|
||||
this.propogatedTrades = 0;
|
||||
this.propogatedTriggers = 0;
|
||||
|
||||
this.warmupCompleted = false;
|
||||
|
||||
this.warmupCandle;
|
||||
}
|
||||
|
||||
PaperTrader.prototype.relayPortfolioChange = function() {
|
||||
this.deferredEmit('portfolioChange', {
|
||||
asset: this.portfolio.asset,
|
||||
currency: this.portfolio.currency
|
||||
});
|
||||
}
|
||||
|
||||
PaperTrader.prototype.relayPortfolioValueChange = function() {
|
||||
this.deferredEmit('portfolioValueChange', {
|
||||
balance: this.getBalance()
|
||||
});
|
||||
}
|
||||
|
||||
PaperTrader.prototype.extractFee = function(amount) {
|
||||
amount *= 1e8;
|
||||
amount *= this.fee;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 1e8;
|
||||
return amount;
|
||||
}
|
||||
|
||||
PaperTrader.prototype.setStartBalance = function() {
|
||||
this.balance = this.getBalance();
|
||||
}
|
||||
|
||||
// after every succesfull trend ride we hopefully end up
|
||||
// with more BTC than we started with, this function
|
||||
// calculates Gekko's profit in %.
|
||||
PaperTrader.prototype.updatePosition = function(what) {
|
||||
|
||||
let cost;
|
||||
let amount;
|
||||
|
||||
// virtually trade all {currency} to {asset}
|
||||
// at the current price (minus fees)
|
||||
if(what === 'long') {
|
||||
cost = (1 - this.fee) * this.portfolio.currency;
|
||||
this.portfolio.asset += this.extractFee(this.portfolio.currency / this.price);
|
||||
amount = this.portfolio.asset;
|
||||
this.portfolio.currency = 0;
|
||||
|
||||
this.exposed = true;
|
||||
this.trades++;
|
||||
}
|
||||
|
||||
// virtually trade all {currency} to {asset}
|
||||
// at the current price (minus fees)
|
||||
else if(what === 'short') {
|
||||
cost = (1 - this.fee) * (this.portfolio.asset * this.price);
|
||||
this.portfolio.currency += this.extractFee(this.portfolio.asset * this.price);
|
||||
amount = this.portfolio.currency / this.price;
|
||||
this.portfolio.asset = 0;
|
||||
|
||||
this.exposed = false;
|
||||
this.trades++;
|
||||
}
|
||||
|
||||
const effectivePrice = this.price * this.fee;
|
||||
|
||||
return { cost, amount, effectivePrice };
|
||||
}
|
||||
|
||||
PaperTrader.prototype.getBalance = function() {
|
||||
return this.portfolio.currency + this.price * this.portfolio.asset;
|
||||
}
|
||||
|
||||
PaperTrader.prototype.now = function() {
|
||||
return this.candle.start.clone().add(1, 'minute');
|
||||
}
|
||||
|
||||
PaperTrader.prototype.processAdvice = function(advice) {
|
||||
let action;
|
||||
if(advice.recommendation === 'short') {
|
||||
action = 'sell';
|
||||
|
||||
// clean up potential old stop trigger
|
||||
if(this.activeStopTrigger) {
|
||||
this.deferredEmit('triggerAborted', {
|
||||
id: this.activeStopTrigger.id,
|
||||
date: advice.date
|
||||
});
|
||||
|
||||
delete this.activeStopTrigger;
|
||||
}
|
||||
|
||||
} else if(advice.recommendation === 'long') {
|
||||
action = 'buy';
|
||||
|
||||
if(advice.trigger) {
|
||||
|
||||
// clean up potential old stop trigger
|
||||
if(this.activeStopTrigger) {
|
||||
this.deferredEmit('triggerAborted', {
|
||||
id: this.activeStopTrigger.id,
|
||||
date: advice.date
|
||||
});
|
||||
|
||||
delete this.activeStopTrigger;
|
||||
}
|
||||
|
||||
this.createTrigger(advice);
|
||||
}
|
||||
} else {
|
||||
return log.warn(
|
||||
`[Papertrader] ignoring unknown advice recommendation: ${advice.recommendation}`
|
||||
);
|
||||
}
|
||||
|
||||
this.tradeId = 'trade-' + (++this.propogatedTrades);
|
||||
|
||||
this.deferredEmit('tradeInitiated', {
|
||||
id: this.tradeId,
|
||||
adviceId: advice.id,
|
||||
action,
|
||||
portfolio: _.clone(this.portfolio),
|
||||
balance: this.getBalance(),
|
||||
date: advice.date,
|
||||
});
|
||||
|
||||
const { cost, amount, effectivePrice } = this.updatePosition(advice.recommendation);
|
||||
|
||||
this.relayPortfolioChange();
|
||||
this.relayPortfolioValueChange();
|
||||
|
||||
this.deferredEmit('tradeCompleted', {
|
||||
id: this.tradeId,
|
||||
adviceId: advice.id,
|
||||
action,
|
||||
cost,
|
||||
amount,
|
||||
price: this.price,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.getBalance(),
|
||||
date: advice.date,
|
||||
effectivePrice,
|
||||
feePercent: this.rawFee
|
||||
});
|
||||
}
|
||||
|
||||
PaperTrader.prototype.createTrigger = function(advice) {
|
||||
const trigger = advice.trigger;
|
||||
|
||||
if(trigger && trigger.type === 'trailingStop') {
|
||||
|
||||
if(!trigger.trailValue) {
|
||||
return log.warn(`[Papertrader] ignoring trailing stop without trail value`);
|
||||
}
|
||||
|
||||
const triggerId = 'trigger-' + (++this.propogatedTriggers);
|
||||
|
||||
this.deferredEmit('triggerCreated', {
|
||||
id: triggerId,
|
||||
at: advice.date,
|
||||
type: 'trailingStop',
|
||||
proprties: {
|
||||
trail: trigger.trailValue,
|
||||
initialPrice: this.price,
|
||||
}
|
||||
});
|
||||
|
||||
this.activeStopTrigger = {
|
||||
id: triggerId,
|
||||
adviceId: advice.id,
|
||||
instance: new TrailingStop({
|
||||
initialPrice: this.price,
|
||||
trail: trigger.trailValue,
|
||||
onTrigger: this.onStopTrigger
|
||||
})
|
||||
}
|
||||
} else {
|
||||
log.warn(`[Papertrader] Gekko does not know trigger with type "${trigger.type}".. Ignoring stop.`);
|
||||
}
|
||||
}
|
||||
|
||||
PaperTrader.prototype.onStopTrigger = function() {
|
||||
|
||||
const date = this.now();
|
||||
|
||||
this.deferredEmit('triggerFired', {
|
||||
id: this.activeStopTrigger.id,
|
||||
date
|
||||
});
|
||||
|
||||
const { cost, amount, effectivePrice } = this.updatePosition('short');
|
||||
|
||||
this.relayPortfolioChange();
|
||||
this.relayPortfolioValueChange();
|
||||
|
||||
this.deferredEmit('tradeCompleted', {
|
||||
id: this.tradeId,
|
||||
adviceId: this.activeStopTrigger.adviceId,
|
||||
action: 'sell',
|
||||
cost,
|
||||
amount,
|
||||
price: this.price,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.getBalance(),
|
||||
date,
|
||||
effectivePrice,
|
||||
feePercent: this.rawFee
|
||||
});
|
||||
|
||||
delete this.activeStopTrigger;
|
||||
}
|
||||
|
||||
PaperTrader.prototype.processStratWarmupCompleted = function() {
|
||||
this.warmupCompleted = true;
|
||||
this.processCandle(this.warmupCandle, _.noop);
|
||||
}
|
||||
|
||||
PaperTrader.prototype.processCandle = function(candle, done) {
|
||||
if(!this.warmupCompleted) {
|
||||
this.warmupCandle = candle;
|
||||
return done();
|
||||
}
|
||||
|
||||
this.price = candle.close;
|
||||
this.candle = candle;
|
||||
|
||||
if(!this.balance) {
|
||||
this.setStartBalance();
|
||||
this.relayPortfolioChange();
|
||||
this.relayPortfolioValueChange();
|
||||
}
|
||||
|
||||
if(this.exposed) {
|
||||
this.relayPortfolioValueChange();
|
||||
}
|
||||
|
||||
if(this.activeStopTrigger) {
|
||||
this.activeStopTrigger.instance.updatePrice(this.price);
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
module.exports = PaperTrader;
|
||||
@@ -0,0 +1,134 @@
|
||||
// log trade performance results
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const humanizeDuration = require('humanize-duration');
|
||||
|
||||
const util = require('../../core/util.js');
|
||||
const dirs = util.dirs();
|
||||
const mode = util.gekkoMode();
|
||||
const log = require(dirs.core + 'log');
|
||||
|
||||
const Logger = function(watchConfig) {
|
||||
this.currency = watchConfig.currency;
|
||||
this.asset = watchConfig.asset;
|
||||
|
||||
this.roundtrips = [];
|
||||
}
|
||||
|
||||
Logger.prototype.round = function(amount) {
|
||||
return amount.toFixed(8);
|
||||
}
|
||||
|
||||
// used for:
|
||||
// - realtime logging (per advice)
|
||||
// - backtest logging (on finalize)
|
||||
Logger.prototype.logReport = function(trade, report) {
|
||||
// ignore the trade
|
||||
|
||||
var start = this.round(report.startBalance);
|
||||
var current = this.round(report.balance);
|
||||
|
||||
log.info(`(PROFIT REPORT) original balance:\t\t ${start} ${this.currency}`);
|
||||
log.info(`(PROFIT REPORT) current balance:\t\t ${current} ${this.currency}`);
|
||||
log.info(
|
||||
`(PROFIT REPORT) profit:\t\t\t\t ${this.round(report.profit)} ${this.currency}`,
|
||||
`(${this.round(report.relativeProfit)}%)`
|
||||
);
|
||||
}
|
||||
|
||||
Logger.prototype.logRoundtripHeading = function() {
|
||||
log.info('(ROUNDTRIP)', 'entry date (UTC) \texit date (UTC) \texposed duration\tP&L \tprofit');
|
||||
}
|
||||
|
||||
Logger.prototype.logRoundtrip = function(rt) {
|
||||
const display = [
|
||||
rt.entryAt.utc().format('YYYY-MM-DD HH:mm'),
|
||||
rt.exitAt.utc().format('YYYY-MM-DD HH:mm'),
|
||||
(moment.duration(rt.duration).humanize() + " ").slice(0, 16),
|
||||
rt.pnl.toFixed(2),
|
||||
rt.profit.toFixed(2)
|
||||
];
|
||||
|
||||
log.info('(ROUNDTRIP)', display.join('\t'));
|
||||
}
|
||||
|
||||
if(mode === 'backtest') {
|
||||
// we only want to log a summarized one line report, like:
|
||||
// 2016-12-19 20:12:00: Paper trader simulated a BUY 0.000 USDT => 1.098 BTC
|
||||
Logger.prototype.handleTrade = function(trade) {
|
||||
if(trade.action !== 'sell' && trade.action !== 'buy')
|
||||
return;
|
||||
|
||||
var at = trade.date.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
|
||||
if(trade.action === 'sell')
|
||||
|
||||
log.info(
|
||||
`${at}: Paper trader simulated a SELL`,
|
||||
`\t${this.round(trade.portfolio.currency)}`,
|
||||
`${this.currency} <= ${this.round(trade.portfolio.asset)}`,
|
||||
`${this.asset}`
|
||||
);
|
||||
|
||||
else if(trade.action === 'buy')
|
||||
|
||||
log.info(
|
||||
`${at}: Paper trader simulated a BUY`,
|
||||
`\t${this.round(trade.portfolio.currency)}`,
|
||||
`${this.currency}\t=> ${this.round(trade.portfolio.asset)}`,
|
||||
`${this.asset}`
|
||||
);
|
||||
}
|
||||
|
||||
Logger.prototype.finalize = function(report) {
|
||||
|
||||
log.info();
|
||||
log.info('(ROUNDTRIP) REPORT:');
|
||||
|
||||
this.logRoundtripHeading();
|
||||
_.each(this.roundtrips, this.logRoundtrip, this);
|
||||
|
||||
log.info()
|
||||
log.info(`(PROFIT REPORT) start time:\t\t\t ${report.startTime}`);
|
||||
log.info(`(PROFIT REPORT) end time:\t\t\t ${report.endTime}`);
|
||||
log.info(`(PROFIT REPORT) timespan:\t\t\t ${report.timespan}`);
|
||||
log.info(`(PROFIT REPORT) exposure:\t\t\t ${report.exposure}`);
|
||||
log.info();
|
||||
log.info(`(PROFIT REPORT) start price:\t\t\t ${report.startPrice} ${this.currency}`);
|
||||
log.info(`(PROFIT REPORT) end price:\t\t\t ${report.endPrice} ${this.currency}`);
|
||||
log.info(`(PROFIT REPORT) Market:\t\t\t\t ${this.round(report.market)}%`);
|
||||
log.info();
|
||||
log.info(`(PROFIT REPORT) amount of trades:\t\t ${report.trades}`);
|
||||
|
||||
this.logReport(null, report);
|
||||
|
||||
log.info(
|
||||
`(PROFIT REPORT) simulated yearly profit:\t ${report.yearlyProfit}`,
|
||||
`${this.currency} (${report.relativeYearlyProfit}%)`
|
||||
);
|
||||
|
||||
log.info(`(PROFIT REPORT) sharpe ratio:\t\t\t ${report.sharpe}`);
|
||||
log.info(`(PROFIT REPORT) expected downside:\t\t ${report.downside}`);
|
||||
log.info(`(PROFIT REPORT) ratio roundtrips:\t\t ${report.ratioRoundTrips}%`);
|
||||
}
|
||||
|
||||
Logger.prototype.handleRoundtrip = function(rt) {
|
||||
this.roundtrips.push(rt);
|
||||
}
|
||||
|
||||
} else if(mode === 'realtime') {
|
||||
Logger.prototype.handleTrade = Logger.prototype.logReport;
|
||||
|
||||
Logger.prototype.handleRoundtrip = function(rt) {
|
||||
this.logRoundtripHeading();
|
||||
this.logRoundtrip(rt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = Logger;
|
||||
@@ -0,0 +1,251 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const statslite = require('stats-lite');
|
||||
const util = require('../../core/util');
|
||||
const log = require(util.dirs().core + 'log')
|
||||
const ENV = util.gekkoEnv();
|
||||
|
||||
const config = util.getConfig();
|
||||
const perfConfig = config.performanceAnalyzer;
|
||||
const watchConfig = config.watch;
|
||||
|
||||
const Logger = require('./logger');
|
||||
|
||||
const PerformanceAnalyzer = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
this.dates = {
|
||||
start: false,
|
||||
end: false
|
||||
}
|
||||
|
||||
this.startPrice = 0;
|
||||
this.endPrice = 0;
|
||||
|
||||
this.currency = watchConfig.currency;
|
||||
this.asset = watchConfig.asset;
|
||||
|
||||
this.logger = new Logger(watchConfig);
|
||||
|
||||
this.trades = 0;
|
||||
|
||||
this.exposure = 0;
|
||||
|
||||
this.roundTrips = [];
|
||||
this.losses = [];
|
||||
this.roundTrip = {
|
||||
id: 0,
|
||||
entry: false,
|
||||
exit: false
|
||||
}
|
||||
|
||||
this.portfolio = {};
|
||||
this.balance;
|
||||
|
||||
this.start = {};
|
||||
this.openRoundTrip = false;
|
||||
|
||||
this.warmupCompleted = false;
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processPortfolioValueChange = function(event) {
|
||||
if(!this.start.balance) {
|
||||
this.start.balance = event.balance;
|
||||
}
|
||||
|
||||
this.balance = event.balance;
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processPortfolioChange = function(event) {
|
||||
if(!this.start.portfolio) {
|
||||
this.start.portfolio = event;
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processStratWarmupCompleted = function() {
|
||||
this.warmupCompleted = true;
|
||||
this.processCandle(this.warmupCandle, _.noop);
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processCandle = function(candle, done) {
|
||||
if(!this.warmupCompleted) {
|
||||
this.warmupCandle = candle;
|
||||
return done();
|
||||
}
|
||||
|
||||
this.price = candle.close;
|
||||
this.dates.end = candle.start.clone().add(1, 'minute');
|
||||
|
||||
if(!this.dates.start) {
|
||||
this.dates.start = candle.start;
|
||||
this.startPrice = candle.close;
|
||||
}
|
||||
|
||||
this.endPrice = candle.close;
|
||||
|
||||
if(this.openRoundTrip) {
|
||||
this.emitRoundtripUpdate();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.emitRoundtripUpdate = function() {
|
||||
const uPnl = this.price - this.roundTrip.entry.price;
|
||||
|
||||
this.deferredEmit('roundtripUpdate', {
|
||||
at: this.dates.end,
|
||||
duration: this.dates.end.diff(this.roundTrip.entry.date),
|
||||
uPnl,
|
||||
uProfit: uPnl / this.roundTrip.entry.total * 100
|
||||
})
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.processTradeCompleted = function(trade) {
|
||||
this.trades++;
|
||||
this.portfolio = trade.portfolio;
|
||||
this.balance = trade.balance;
|
||||
|
||||
this.registerRoundtripPart(trade);
|
||||
|
||||
const report = this.calculateReportStatistics();
|
||||
if(report) {
|
||||
this.logger.handleTrade(trade, report);
|
||||
this.deferredEmit('performanceReport', report);
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.registerRoundtripPart = function(trade) {
|
||||
if(this.trades === 1 && trade.action === 'sell') {
|
||||
// this is not part of a valid roundtrip
|
||||
return;
|
||||
}
|
||||
|
||||
if(trade.action === 'buy') {
|
||||
if (this.roundTrip.exit) {
|
||||
this.roundTrip.id++;
|
||||
this.roundTrip.exit = false
|
||||
}
|
||||
|
||||
this.roundTrip.entry = {
|
||||
date: trade.date,
|
||||
price: trade.price,
|
||||
total: trade.portfolio.currency + (trade.portfolio.asset * trade.price),
|
||||
}
|
||||
this.openRoundTrip = true;
|
||||
} else if(trade.action === 'sell') {
|
||||
this.roundTrip.exit = {
|
||||
date: trade.date,
|
||||
price: trade.price,
|
||||
total: trade.portfolio.currency + (trade.portfolio.asset * trade.price),
|
||||
}
|
||||
this.openRoundTrip = false;
|
||||
|
||||
this.handleCompletedRoundtrip();
|
||||
}
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.handleCompletedRoundtrip = function() {
|
||||
var roundtrip = {
|
||||
id: this.roundTrip.id,
|
||||
|
||||
entryAt: this.roundTrip.entry.date,
|
||||
entryPrice: this.roundTrip.entry.price,
|
||||
entryBalance: this.roundTrip.entry.total,
|
||||
|
||||
exitAt: this.roundTrip.exit.date,
|
||||
exitPrice: this.roundTrip.exit.price,
|
||||
exitBalance: this.roundTrip.exit.total,
|
||||
|
||||
duration: this.roundTrip.exit.date.diff(this.roundTrip.entry.date)
|
||||
}
|
||||
|
||||
roundtrip.pnl = roundtrip.exitBalance - roundtrip.entryBalance;
|
||||
roundtrip.profit = (100 * roundtrip.exitBalance / roundtrip.entryBalance) - 100;
|
||||
|
||||
this.roundTrips[this.roundTrip.id] = roundtrip;
|
||||
|
||||
this.logger.handleRoundtrip(roundtrip);
|
||||
|
||||
this.deferredEmit('roundtrip', roundtrip);
|
||||
|
||||
// update cached exposure
|
||||
this.exposure = this.exposure + Date.parse(this.roundTrip.exit.date) - Date.parse(this.roundTrip.entry.date);
|
||||
// track losses separately for downside report
|
||||
if (roundtrip.exitBalance < roundtrip.entryBalance)
|
||||
this.losses.push(roundtrip);
|
||||
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.calculateReportStatistics = function() {
|
||||
if(!this.start.balance || !this.start.portfolio) {
|
||||
log.error('Cannot calculate a profit report without having received portfolio data.');
|
||||
log.error('Skipping performanceReport..');
|
||||
return false;
|
||||
}
|
||||
|
||||
// the portfolio's balance is measured in {currency}
|
||||
const profit = this.balance - this.start.balance;
|
||||
|
||||
const timespan = moment.duration(
|
||||
this.dates.end.diff(this.dates.start)
|
||||
);
|
||||
const relativeProfit = this.balance / this.start.balance * 100 - 100;
|
||||
const relativeYearlyProfit = relativeProfit / timespan.asYears();
|
||||
|
||||
const percentExposure = this.exposure / (Date.parse(this.dates.end) - Date.parse(this.dates.start));
|
||||
|
||||
const sharpe = (relativeYearlyProfit - perfConfig.riskFreeReturn)
|
||||
/ statslite.stdev(this.roundTrips.map(r => r.profit))
|
||||
/ Math.sqrt(this.trades / (this.trades - 2));
|
||||
|
||||
const downside = statslite.percentile(this.losses.map(r => r.profit), 0.25)
|
||||
* Math.sqrt(this.trades / (this.trades - 2));
|
||||
|
||||
const ratioRoundTrips = this.roundTrips.length > 0 ? (this.roundTrips.filter(roundTrip => roundTrip.pnl > 0 ).length / this.roundTrips.length * 100).toFixed(4) : 100;
|
||||
|
||||
const report = {
|
||||
startTime: this.dates.start.utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: this.dates.end.utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
timespan: timespan.humanize(),
|
||||
market: this.endPrice * 100 / this.startPrice - 100,
|
||||
|
||||
balance: this.balance,
|
||||
profit,
|
||||
relativeProfit: relativeProfit,
|
||||
|
||||
yearlyProfit: profit / timespan.asYears(),
|
||||
relativeYearlyProfit,
|
||||
|
||||
startPrice: this.startPrice,
|
||||
endPrice: this.endPrice,
|
||||
trades: this.trades,
|
||||
startBalance: this.start.balance,
|
||||
exposure: percentExposure,
|
||||
sharpe,
|
||||
downside,
|
||||
ratioRoundTrips
|
||||
}
|
||||
|
||||
report.alpha = report.relativeProfit - report.market;
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
PerformanceAnalyzer.prototype.finalize = function(done) {
|
||||
if(!this.trades) {
|
||||
return done();
|
||||
}
|
||||
|
||||
const report = this.calculateReportStatistics();
|
||||
if(report) {
|
||||
this.logger.finalize(report);
|
||||
this.emit('performanceReport', report);
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
|
||||
module.exports = PerformanceAnalyzer;
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
|
||||
const util = require('../../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const dirs = util.dirs();
|
||||
|
||||
const log = require(util.dirs().core + 'log');
|
||||
const postgresUtil = require('./util');
|
||||
|
||||
const adapter = config.postgresql;
|
||||
|
||||
// verify the correct dependencies are installed
|
||||
const pluginHelper = require(dirs.core + 'pluginUtil');
|
||||
const pluginMock = {
|
||||
slug: 'postgresql adapter',
|
||||
dependencies: config.postgresql.dependencies
|
||||
}
|
||||
|
||||
const cannotLoad = pluginHelper.cannotLoad(pluginMock);
|
||||
if(cannotLoad) {
|
||||
util.die(cannotLoad);
|
||||
}
|
||||
|
||||
const pg = require('pg');
|
||||
|
||||
const version = adapter.version;
|
||||
|
||||
const dbName = postgresUtil.database();
|
||||
|
||||
const mode = util.gekkoMode();
|
||||
|
||||
const connectionString = config.postgresql.connectionString;
|
||||
|
||||
const checkClient = new pg.Pool({
|
||||
connectionString: connectionString + '/postgres',
|
||||
});
|
||||
const pool = new pg.Pool({
|
||||
connectionString: connectionString + '/' + dbName,
|
||||
});
|
||||
|
||||
// We need to check if the db exists first.
|
||||
// This requires connecting to the default
|
||||
// postgres database first. Your postgres
|
||||
// user will need appropriate rights.
|
||||
checkClient.connect((err, client, done) => {
|
||||
if(err) {
|
||||
util.die(err);
|
||||
}
|
||||
|
||||
log.debug("Check database exists: " + dbName);
|
||||
client.query("select count(*) from pg_catalog.pg_database where datname = $1", [dbName],
|
||||
(err, res) => {
|
||||
if(err) {
|
||||
util.die(err);
|
||||
}
|
||||
|
||||
if(res.rows[0].count !== '0') {
|
||||
// database exists
|
||||
log.debug("Database exists: " + dbName);
|
||||
log.debug("Postgres connection pool is ready, db " + dbName);
|
||||
upsertTables();
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
// database dot NOT exist
|
||||
|
||||
if(mode === 'backtest') {
|
||||
// no point in trying to backtest with
|
||||
// non existing data.
|
||||
util.die(`History does not exist for exchange ${config.watch.exchange}.`);
|
||||
}
|
||||
|
||||
createDatabase(client, done);
|
||||
});
|
||||
});
|
||||
|
||||
const createDatabase = (client, done) => {
|
||||
client.query("CREATE DATABASE " + dbName, err => {
|
||||
if(err) {
|
||||
util.die(err);
|
||||
}
|
||||
|
||||
log.debug("Postgres connection pool is ready, db " + dbName);
|
||||
done();
|
||||
upsertTables();
|
||||
});
|
||||
}
|
||||
|
||||
const upsertTables = () => {
|
||||
const upsertQuery =
|
||||
`CREATE TABLE IF NOT EXISTS
|
||||
${postgresUtil.table('candles')} (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
start integer UNIQUE,
|
||||
open double precision NOT NULL,
|
||||
high double precision NOT NULL,
|
||||
low double precision NOT NULL,
|
||||
close double precision NOT NULL,
|
||||
vwp double precision NOT NULL,
|
||||
volume double precision NOT NULL,
|
||||
trades INTEGER NOT NULL
|
||||
);`;
|
||||
|
||||
pool.query(upsertQuery, (err) => {
|
||||
if(err) {
|
||||
util.die(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = pool;
|
||||
Executable
+209
@@ -0,0 +1,209 @@
|
||||
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 postgresUtil = require('./util');
|
||||
|
||||
const { Query } = require('pg');
|
||||
|
||||
var Reader = function() {
|
||||
_.bindAll(this);
|
||||
this.db = handle;
|
||||
}
|
||||
|
||||
// returns the furthest point (up to `from`) in time we have valid data from
|
||||
Reader.prototype.mostRecentWindow = function(from, to, next) {
|
||||
to = to.unix();
|
||||
from = from.unix();
|
||||
|
||||
var maxAmount = to - from + 1;
|
||||
|
||||
this.db.connect((err, client, done) => {
|
||||
|
||||
if(err) {
|
||||
log.error(err);
|
||||
return util.die(err.message);
|
||||
}
|
||||
|
||||
var query = client.query(new Query(`
|
||||
SELECT start from ${postgresUtil.table('candles')}
|
||||
WHERE start <= ${to} AND start >= ${from}
|
||||
ORDER BY start DESC
|
||||
`), function (err, result) {
|
||||
if (err) {
|
||||
// bail out if the table does not exist
|
||||
if (err.message.indexOf(' does not exist') !== -1)
|
||||
return next(false);
|
||||
|
||||
log.error(err);
|
||||
return util.die('DB error while reading mostRecentWindow');
|
||||
}
|
||||
});
|
||||
|
||||
var rows = [];
|
||||
query.on('row', function(row) {
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
// After all data is returned, close connection and return results
|
||||
query.on('end', function() {
|
||||
done();
|
||||
// no candles are available
|
||||
if(rows.length === 0) {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
if(rows.length === maxAmount) {
|
||||
|
||||
// full history is available!
|
||||
|
||||
return next({
|
||||
from: from,
|
||||
to: to
|
||||
});
|
||||
}
|
||||
|
||||
// we have at least one gap, figure out where
|
||||
var mostRecent = _.first(rows).start;
|
||||
|
||||
var gapIndex = _.findIndex(rows, function(r, i) {
|
||||
return r.start !== mostRecent - i * 60;
|
||||
});
|
||||
|
||||
// if there was no gap in the records, but
|
||||
// there were not enough records.
|
||||
if(gapIndex === -1) {
|
||||
var leastRecent = _.last(rows).start;
|
||||
return next({
|
||||
from: leastRecent,
|
||||
to: mostRecent
|
||||
});
|
||||
}
|
||||
|
||||
// else return mostRecent and the
|
||||
// the minute before the gap
|
||||
return next({
|
||||
from: rows[ gapIndex - 1 ].start,
|
||||
to: mostRecent
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.tableExists = function (name, next) {
|
||||
this.db.connect((err,client,done) => {
|
||||
client.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema='${postgresUtil.schema()}'
|
||||
AND table_name='${postgresUtil.table(name)}';
|
||||
`, function(err, result) {
|
||||
done();
|
||||
if (err) {
|
||||
return util.die('DB error at `tableExists`');
|
||||
}
|
||||
|
||||
next(null, result.rows.length === 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.get = function(from, to, what, next) {
|
||||
if(what === 'full'){
|
||||
what = '*';
|
||||
}
|
||||
|
||||
this.db.connect((err,client,done) => {
|
||||
var query = client.query(new Query(`
|
||||
SELECT ${what} from ${postgresUtil.table('candles')}
|
||||
WHERE start <= ${to} AND start >= ${from}
|
||||
ORDER BY start ASC
|
||||
`));
|
||||
|
||||
var rows = [];
|
||||
query.on('row', function(row) {
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
query.on('end',function(){
|
||||
done();
|
||||
next(null, rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.count = function(from, to, next) {
|
||||
this.db.connect((err,client,done) => {
|
||||
if(err) {
|
||||
log.error(err);
|
||||
return util.die(err.message);
|
||||
}
|
||||
|
||||
var query = client.query(new Query(`
|
||||
SELECT COUNT(*) as count from ${postgresUtil.table('candles')}
|
||||
WHERE start <= ${to} AND start >= ${from}
|
||||
`));
|
||||
var rows = [];
|
||||
query.on('row', function(row) {
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
query.on('end',function(){
|
||||
done();
|
||||
next(null, _.first(rows).count);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.countTotal = function(next) {
|
||||
this.db.connect((err,client,done) => {
|
||||
var query = client.query(new Query(`
|
||||
SELECT COUNT(*) as count from ${postgresUtil.table('candles')}
|
||||
`));
|
||||
var rows = [];
|
||||
query.on('row', function(row) {
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
query.on('end',function(){
|
||||
done();
|
||||
next(null, _.first(rows).count);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.getBoundry = function(next) {
|
||||
this.db.connect((err,client,done) => {
|
||||
var query = client.query(new Query(`
|
||||
SELECT (
|
||||
SELECT start
|
||||
FROM ${postgresUtil.table('candles')}
|
||||
ORDER BY start LIMIT 1
|
||||
) as first,
|
||||
(
|
||||
SELECT start
|
||||
FROM ${postgresUtil.table('candles')}
|
||||
ORDER BY start DESC
|
||||
LIMIT 1
|
||||
) as last
|
||||
`));
|
||||
var rows = [];
|
||||
query.on('row', function(row) {
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
query.on('end',function(){
|
||||
done();
|
||||
next(null, _.first(rows));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reader.prototype.close = function() {
|
||||
//obsolete due to connection pooling
|
||||
//this.db.end();
|
||||
}
|
||||
|
||||
module.exports = Reader;
|
||||
@@ -0,0 +1,93 @@
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
var pg = require('pg');
|
||||
|
||||
const util = require('../../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const dirs = util.dirs();
|
||||
var postgresUtil = require('./util');
|
||||
|
||||
var connectionString = config.postgresql.connectionString;
|
||||
|
||||
|
||||
module.exports = done => {
|
||||
var scanClient = new pg.Client(connectionString+"/postgres");
|
||||
|
||||
let markets = [];
|
||||
|
||||
scanClient.connect(function (err) {
|
||||
if(err){
|
||||
util.die(err);
|
||||
}
|
||||
|
||||
var sql = "select datname from pg_database";
|
||||
|
||||
// In single DB setup we don't need to go look into other DBs
|
||||
if (postgresUtil.useSingleDatabase()) {
|
||||
sql = "select datname from pg_database where datname='" + postgresUtil.database() + "'";
|
||||
}
|
||||
|
||||
var query = scanClient.query(sql, function (err, result) {
|
||||
|
||||
async.each(result.rows, (dbRow, next) => {
|
||||
|
||||
var scanTablesClient = new pg.Client(connectionString + "/" + dbRow.datname);
|
||||
var dbName = dbRow.datname;
|
||||
|
||||
scanTablesClient.connect(function (err) {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var query = scanTablesClient.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema='${postgresUtil.schema()}';
|
||||
`, function(err, result) {
|
||||
if (err) {
|
||||
return util.die('DB error at `scanning tables`');
|
||||
}
|
||||
|
||||
_.each(result.rows, table => {
|
||||
let parts = table.table_name.split('_');
|
||||
let first = parts.shift();
|
||||
let exchangeName = dbName;
|
||||
|
||||
/**
|
||||
* If using single database, we need to strip
|
||||
* exchange from table name. See here how tables
|
||||
* are named:
|
||||
*
|
||||
* - in single database setup: poloniex_candles_usdt_btc
|
||||
* - in multi db setup: db = poloniex, table = candles_usdt_btc
|
||||
*/
|
||||
if (postgresUtil.useSingleDatabase()) {
|
||||
exchangeName = first;
|
||||
first = parts.shift();
|
||||
}
|
||||
|
||||
if(first === 'candles')
|
||||
markets.push({
|
||||
exchange: exchangeName,
|
||||
currency: _.first(parts),
|
||||
asset: _.last(parts)
|
||||
});
|
||||
});
|
||||
|
||||
scanTablesClient.end();
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
// got all tables!
|
||||
err => {
|
||||
scanClient.end();
|
||||
done(err, markets);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
var config = require('../../core/util.js').getConfig();
|
||||
|
||||
var watch = config.watch;
|
||||
if(watch) {
|
||||
var settings = {
|
||||
exchange: watch.exchange,
|
||||
pair: [watch.currency, watch.asset]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we use single database where
|
||||
* all our tables are stored. The default is to store
|
||||
* every exchange into it's own db.
|
||||
*
|
||||
* Set config.postgresql.database to use single db setup
|
||||
*/
|
||||
function useSingleDatabase() {
|
||||
return !!config.postgresql.database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postgres has tables in lowercase if you don't
|
||||
* escape their names. Which we don't and so let's
|
||||
* just lowercase them.
|
||||
*/
|
||||
function useLowerCaseTableNames() {
|
||||
return !config.postgresql.noLowerCaseTableName;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
settings: settings,
|
||||
|
||||
// true if we have single db setup (see postrgesql.database config key)
|
||||
useSingleDatabase: useSingleDatabase,
|
||||
|
||||
// returns DB name (depends on single db setup)
|
||||
database: function () {
|
||||
return useSingleDatabase() ?
|
||||
config.postgresql.database :
|
||||
config.watch.exchange.toLowerCase().replace(/\-/g,'');
|
||||
},
|
||||
|
||||
// returns table name which can be different if we use
|
||||
// single or multiple db setup.
|
||||
table: function (name) {
|
||||
if (useSingleDatabase()) {
|
||||
name = watch.exchange.replace(/\-/g,'') + '_' + name;
|
||||
}
|
||||
var fullName = [name, settings.pair.join('_')].join('_');
|
||||
return useLowerCaseTableNames() ? fullName.toLowerCase() : fullName;
|
||||
},
|
||||
|
||||
startconstraint: function (name) {
|
||||
if (useSingleDatabase()) {
|
||||
name = watch.exchange.replace(/\-/g,'') + '_' + name;
|
||||
}
|
||||
var fullName = [name, settings.pair.join('_')].join('_');
|
||||
return useLowerCaseTableNames() ? fullName.toLowerCase() + '_start_key' : fullName + '_start_key';
|
||||
},
|
||||
|
||||
// postgres schema name. defaults to 'public'
|
||||
schema: function () {
|
||||
return config.postgresql.schema ? config.postgresql.schema : 'public';
|
||||
}
|
||||
}
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
var _ = require('lodash');
|
||||
const log = require('../../core/log');
|
||||
const util = require('../../core/util');
|
||||
const config = util.getConfig();
|
||||
|
||||
var handle = require('./handle');
|
||||
var postgresUtil = require('./util');
|
||||
|
||||
var Store = function(done, pluginMeta) {
|
||||
_.bindAll(this);
|
||||
this.done = done;
|
||||
this.db = handle;
|
||||
this.cache = [];
|
||||
done();
|
||||
}
|
||||
|
||||
Store.prototype.writeCandles = function() {
|
||||
if(_.isEmpty(this.cache)){
|
||||
return;
|
||||
}
|
||||
|
||||
//log.debug('Writing candles to DB!');
|
||||
_.each(this.cache, candle => {
|
||||
var stmt = `
|
||||
BEGIN;
|
||||
LOCK TABLE ${postgresUtil.table('candles')} IN SHARE ROW EXCLUSIVE MODE;
|
||||
INSERT INTO ${postgresUtil.table('candles')}
|
||||
(start, open, high,low, close, vwp, volume, trades)
|
||||
VALUES
|
||||
(${candle.start.unix()}, ${candle.open}, ${candle.high}, ${candle.low}, ${candle.close}, ${candle.vwp}, ${candle.volume}, ${candle.trades})
|
||||
ON CONFLICT ON CONSTRAINT ${postgresUtil.startconstraint('candles')}
|
||||
DO NOTHING;
|
||||
COMMIT;
|
||||
`;
|
||||
|
||||
this.db.connect((err,client,done) => {
|
||||
if(err) {
|
||||
util.die(err);
|
||||
}
|
||||
client.query(stmt, (err, res) => {
|
||||
done();
|
||||
if (err) {
|
||||
log.debug(err.stack)
|
||||
} else {
|
||||
//log.debug(res)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.cache = [];
|
||||
}
|
||||
|
||||
var processCandle = function(candle, done) {
|
||||
this.cache.push(candle);
|
||||
if (this.cache.length > 1)
|
||||
this.writeCandles();
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
var finalize = function(done) {
|
||||
this.writeCandles();
|
||||
this.db = null;
|
||||
done();
|
||||
}
|
||||
|
||||
if(config.candleWriter.enabled) {
|
||||
Store.prototype.processCandle = processCandle;
|
||||
Store.prototype.finalize = finalize;
|
||||
}
|
||||
|
||||
module.exports = Store;
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Created by rocketman1337345 on 8/14/16.
|
||||
* Extended by RJPGriffin (Gryphon) on 30/5/18
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
Required Config:
|
||||
|
||||
config.pushbullet = {
|
||||
// sends pushbullets if true
|
||||
enabled: true,
|
||||
// Send 'Gekko starting' message if true
|
||||
sendMessageOnStart: true,
|
||||
// Send Message for advice? Recommend Flase for paper, true for live
|
||||
sendOnAdvice: true,
|
||||
// Send Message on Trade Completion?
|
||||
sendOnTrade: true,
|
||||
// For Overall P/L calc. Pass in old balance if desired, else leave '0'
|
||||
startingBalance: '0',
|
||||
// your pushbullet API key
|
||||
key: '',
|
||||
// your email
|
||||
email: '[email protected]',
|
||||
// Messages will start with this tag
|
||||
tag: '[GEKKO]'
|
||||
};
|
||||
|
||||
|
||||
**/
|
||||
|
||||
var pushbullet = require("pushbullet");
|
||||
var _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const request = require('request');
|
||||
var log = require('../core/log.js');
|
||||
var util = require('../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var pbConf = config.pushbullet;
|
||||
|
||||
var Pushbullet = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.pusher;
|
||||
this.price = 'N/A';
|
||||
this.startingBalance = pbConf.startingBalance === undefined ? 0 : Number(pbConf.startingBalance);
|
||||
this.advicePrice = 0;
|
||||
this.adviceTime = moment();
|
||||
this.lastBuyTime = moment();
|
||||
this.lastBuyBalance = 0;
|
||||
this.hasBought = 0;
|
||||
|
||||
this.done = done;
|
||||
this.setup();
|
||||
};
|
||||
|
||||
Pushbullet.prototype.setup = function(done) {
|
||||
|
||||
var setupPushBullet = function(err, result) {
|
||||
if (pbConf.sendMessageOnStart) {
|
||||
var title = pbConf.tag;
|
||||
var exchange = config.watch.exchange;
|
||||
var currency = config.watch.currency;
|
||||
var asset = config.watch.asset;
|
||||
let tradeType = 'watching';
|
||||
if (config.trader.enabled) {
|
||||
tradeType = "Live Trading";
|
||||
}
|
||||
if (config.paperTrader.enabled) {
|
||||
tradeType = "Paper Trading";
|
||||
}
|
||||
|
||||
var body = `Gekko has started ${tradeType} ${asset}/${currency} on ${exchange}.`;
|
||||
|
||||
//If trading Advisor is enabled, add strategy and candle size information
|
||||
if (config.tradingAdvisor.enabled) {
|
||||
body += `\n\nUsing ${config.tradingAdvisor.method} strategy on M${config.tradingAdvisor.candleSize} candles.`
|
||||
}
|
||||
|
||||
this.mail(title, body);
|
||||
} else {
|
||||
log.debug('Skipping Send message on startup')
|
||||
}
|
||||
};
|
||||
setupPushBullet.call(this)
|
||||
};
|
||||
|
||||
Pushbullet.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
|
||||
Pushbullet.prototype.processAdvice = function(advice) {
|
||||
|
||||
this.advicePrice = this.price;
|
||||
this.adviceTime = advice.date;
|
||||
|
||||
if (pbConf.sendOnAdvice) {
|
||||
|
||||
var text = [
|
||||
'Gekko has new advice for ',
|
||||
capF(config.watch.exchange),
|
||||
', advice is to go ',
|
||||
capF(advice.recommendation),
|
||||
'.\n\nThe current ',
|
||||
config.watch.asset,
|
||||
' price is ',
|
||||
this.advicePrice
|
||||
].join('');
|
||||
|
||||
var subject = pbConf.tag + ' New advice: go ' + advice.recommendation;
|
||||
|
||||
this.mail(subject, text);
|
||||
}
|
||||
};
|
||||
|
||||
Pushbullet.prototype.processTradeCompleted = function(trade) {
|
||||
|
||||
//Check Starting balance is initialized - if 0, initialize it
|
||||
this.startingBalance = this.startingBalance ? this.startingBalance : trade.balance;
|
||||
|
||||
// If config variable doesn't exist (old config) defaults to send
|
||||
let sendOnTrade = pbConf.sendOnTrade === undefined ? 1 : pbConf.sendOnTrade;
|
||||
|
||||
if (sendOnTrade) {
|
||||
|
||||
// Calculate exposure Time
|
||||
let exposureTimeStr = '';
|
||||
let balanceChangeStr = '\n';
|
||||
let totBalanceChangeStr = '';
|
||||
let subject = `${pbConf.tag} ${capF(trade.action)} complete`;
|
||||
|
||||
if (trade.action === 'buy') {
|
||||
this.hasBought = 1; // Flag to ensure that the following variables have been filled
|
||||
this.lastBuyTime = trade.date;
|
||||
this.lastBuyBalance = trade.balance;
|
||||
} else if (this.hasBought) { //if sell and we have previous buy data
|
||||
exposureTimeStr = `\nExposure Time: ${moment.duration(trade.date.diff(this.lastBuyTime)).humanize()}`;
|
||||
|
||||
//Calculate balance change
|
||||
let oBal = this.lastBuyBalance; // Old Balance
|
||||
let nBal = trade.balance; // New Balance
|
||||
let diffBal = Math.abs(nBal - oBal); // Balance Difference
|
||||
let percDiffBal = (diffBal / oBal) * 100; // Percentage difference
|
||||
|
||||
|
||||
if (nBal >= oBal) { // profit!
|
||||
balanceChangeStr = `\n\nRound trip profit of: \n${getNumStr(diffBal)}${config.watch.currency} \n${getNumStr(percDiffBal,2)}%\n`
|
||||
subject = `${subject}: +${getNumStr(percDiffBal,2)}%`
|
||||
} else if (nBal < oBal) { // Loss :(
|
||||
balanceChangeStr = `\n\nRound trip loss of: \n-${getNumStr(diffBal)}${config.watch.currency} \n-${getNumStr(percDiffBal,2)}%\n`
|
||||
subject = `${subject}: -${getNumStr(percDiffBal,2)}%`
|
||||
}
|
||||
|
||||
//Calculate overall P/l
|
||||
let sBal = this.startingBalance;
|
||||
let tDiffBal = Math.abs(nBal - sBal);
|
||||
let percDiffTotBal = (tDiffBal / sBal) * 100;
|
||||
if (nBal >= sBal) { // profit!
|
||||
totBalanceChangeStr = `\nOverall gain of: \n${getNumStr(tDiffBal)}${config.watch.currency} \n${getNumStr(percDiffTotBal,2)}%\n`
|
||||
} else if (nBal < sBal) { // Loss :(
|
||||
totBalanceChangeStr = `\nOverall loss of \n-${getNumStr(tDiffBal)}${config.watch.currency} \n-${getNumStr(percDiffTotBal,2)}%\n`
|
||||
|
||||
} else if (trade.action === 'sell' && !this.hasBought) {
|
||||
balanceChangeStr = `\n\nNot enough data for exposure time, round trip or overall performance yet. This will appear after bot has completed first round trip.`
|
||||
}
|
||||
}
|
||||
|
||||
let costOfTradeStr = `\nCost of Trade: ${getNumStr(trade.cost)}${config.watch.currency}, ${getNumStr(((trade.cost / (trade.amount*trade.price)) * 100), 2)}%`;
|
||||
|
||||
//build strings that are only sent for Live trading, not paperTrader
|
||||
let orderFillTimeStr = '';
|
||||
let slippageStr = '';
|
||||
|
||||
if (!config.paperTrader.enabled) {
|
||||
let timeToComplete = moment.duration(trade.date.diff(this.adviceTime)).humanize();
|
||||
orderFillTimeStr = `\nOrder fill Time: ${timeToComplete}`;
|
||||
|
||||
var slip;
|
||||
//Slip direction is opposite for buy and sell
|
||||
if (trade.price === this.advicePrice) {
|
||||
slip = 0;
|
||||
} else if (trade.action === 'buy') {
|
||||
slip = 100 * ((trade.price - this.advicePrice) / this.advicePrice);
|
||||
} else if (trade.action === 'sell') {
|
||||
slip = 100 * ((this.advicePrice - trade.price) / this.advicePrice);
|
||||
}
|
||||
slippageStr = `\nSlipped ${getNumStr(slip,2)}% from advice @ ${getNumStr(this.advicePrice)}`;
|
||||
|
||||
|
||||
}
|
||||
|
||||
var text = [
|
||||
capF(config.watch.exchange), ' ', config.watch.asset, '/', config.watch.currency,
|
||||
`\n\n${config.watch.asset} Trade Price: ${getNumStr(trade.price)}`,
|
||||
`\n${getPastTense(trade.action)} ${getNumStr(trade.amount)} ${config.watch.asset}`,
|
||||
orderFillTimeStr,
|
||||
slippageStr,
|
||||
costOfTradeStr,
|
||||
exposureTimeStr,
|
||||
balanceChangeStr,
|
||||
totBalanceChangeStr,
|
||||
'\nBalance: ', getNumStr(trade.balance), config.watch.currency,
|
||||
].join('');
|
||||
|
||||
this.mail(subject, text);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// A long winded function to make sure numbers aren't displayed with too many decimal places
|
||||
// and are a little humanized
|
||||
function getNumStr(num, fixed = 4) {
|
||||
let numStr = '';
|
||||
|
||||
if (typeof num != "number") {
|
||||
num = Number(num);
|
||||
if (isNaN(num)) {
|
||||
// console.log("Pushbullet Plugin: Number Conversion Failed");
|
||||
return "Conversion Failure";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (Number.isInteger(num)) {
|
||||
numStr = num.toString();
|
||||
} else {
|
||||
|
||||
//Create modNum Max - Must be a better way...
|
||||
let modNumMax = '1';
|
||||
for (let i = 1; i < fixed; i++) {
|
||||
modNumMax = modNumMax + '0';
|
||||
}
|
||||
modNumMax = Number(modNumMax);
|
||||
|
||||
let i = 0;
|
||||
if (num < 1) {
|
||||
let modNum = num - Math.floor(num);
|
||||
while (modNum < modNumMax && i < 8) {
|
||||
modNum *= 10;
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
i = fixed;
|
||||
}
|
||||
numStr = num.toFixed(i);
|
||||
//Remove any excess zeros
|
||||
while (numStr.charAt(numStr.length - 1) === '0') {
|
||||
numStr = numStr.substring(0, numStr.length - 1);
|
||||
}
|
||||
|
||||
//If last char remaining is a decimal point, remove it
|
||||
if (numStr.charAt(numStr.length - 1) === '.') {
|
||||
numStr = numStr.substring(0, numStr.length - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Add commas for thousands etc
|
||||
let dp = numStr.indexOf('.'); //find deciaml point
|
||||
if (dp < 0) { //no dp found
|
||||
dp = numStr.length;
|
||||
}
|
||||
|
||||
let insPos = dp - 3;
|
||||
insCount = 0;
|
||||
while (insPos > 0) {
|
||||
insCount++;
|
||||
numStr = numStr.slice(0, insPos) + ',' + numStr.slice(insPos);
|
||||
insPos -= 3;
|
||||
}
|
||||
|
||||
|
||||
return (numStr);
|
||||
}
|
||||
|
||||
function capF(inWord) { //Capitalise first letter of string
|
||||
return (inWord.charAt(0).toUpperCase() + inWord.slice(1));
|
||||
}
|
||||
|
||||
function getPastTense(action) {
|
||||
let ret = '';
|
||||
if (action === 'buy') {
|
||||
ret = 'Bought'
|
||||
} else if (action === 'sell') {
|
||||
ret = 'Sold'
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Pushbullet.prototype.mail = function(subject, content, done) {
|
||||
var pusher = new pushbullet(pbConf.key);
|
||||
pusher.note(pbConf.email, subject, content, function(error, response) {
|
||||
if (error || !response) {
|
||||
log.error('Pushbullet ERROR:', error)
|
||||
} else if (response && response.active) {
|
||||
log.info('Pushbullet Message Sent')
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Pushbullet.prototype.checkResults = function(err) {
|
||||
if (err)
|
||||
log.warn('error sending pushbullet message', err);
|
||||
else
|
||||
log.info('Send advice via pushbullet message.');
|
||||
};
|
||||
|
||||
module.exports = Pushbullet;
|
||||
@@ -0,0 +1,85 @@
|
||||
var push = require( 'pushover-notifications' );
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log.js');
|
||||
var util = require('../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var pushoverConfig = config.pushover;
|
||||
|
||||
var Pushover = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
this.p;
|
||||
this.price = 'N/A';
|
||||
|
||||
this.setup();
|
||||
}
|
||||
|
||||
Pushover.prototype.setup = function() {
|
||||
var setupPushover = function() {
|
||||
this.p = new push( {
|
||||
user: pushoverConfig.user,
|
||||
token: pushoverConfig.key,
|
||||
});
|
||||
|
||||
if(pushoverConfig.sendPushoverOnStart) {
|
||||
this.send(
|
||||
"Gekko has started",
|
||||
[
|
||||
"I've just started watching ",
|
||||
config.watch.exchange,
|
||||
' ',
|
||||
config.watch.currency,
|
||||
'/',
|
||||
config.watch.asset,
|
||||
". I'll let you know when I got some advice"
|
||||
].join('')
|
||||
);
|
||||
} else
|
||||
log.debug('Setup pushover adviser.');
|
||||
}
|
||||
setupPushover.call(this);
|
||||
}
|
||||
|
||||
Pushover.prototype.send = function(subject, content) {
|
||||
var msg = {
|
||||
// These values correspond to the parameters detailed on https://pushover.net/api
|
||||
// 'message' is required. All other values are optional.
|
||||
message: content,
|
||||
title: pushoverConfig.tag + subject,
|
||||
device: 'devicename',
|
||||
priority: 1
|
||||
};
|
||||
|
||||
this.p.send( msg, function( err, result ) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log( result );
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Pushover.prototype.processCandle = function(candle, callback) {
|
||||
this.price = candle.close;
|
||||
callback();
|
||||
}
|
||||
|
||||
Pushover.prototype.processAdvice = function(advice) {
|
||||
if (advice.recommendation == 'soft' && pushoverConfig.muteSoft) return;
|
||||
var text = [
|
||||
advice.recommendation,
|
||||
this.price
|
||||
].join(' ');
|
||||
var subject = text;
|
||||
this.send(subject, text);
|
||||
}
|
||||
|
||||
Pushover.prototype.checkResults = function(err) {
|
||||
if(err)
|
||||
log.warn('error sending pushover', err);
|
||||
else
|
||||
log.info('Send advice via pushover.');
|
||||
}
|
||||
|
||||
module.exports = Pushover;
|
||||
@@ -0,0 +1,67 @@
|
||||
var log = require('../core/log.js');
|
||||
var util = require('../core/util');
|
||||
var config = util.getConfig();
|
||||
var redisBeacon = config.redisBeacon;
|
||||
var watch = config.watch;
|
||||
|
||||
var subscriptions = require('../subscriptions');
|
||||
var _ = require('lodash');
|
||||
|
||||
var redis = require("redis");
|
||||
|
||||
var Actor = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.market = [
|
||||
watch.exchange,
|
||||
watch.currency,
|
||||
watch.asset
|
||||
].join('-');
|
||||
|
||||
this.init(done);
|
||||
}
|
||||
|
||||
// This actor is dynamically build based on
|
||||
// what the config specifies it should emit.
|
||||
//
|
||||
// This way we limit overhead because Gekko
|
||||
// only binds to events redis is going to
|
||||
// emit.
|
||||
|
||||
var proto = {};
|
||||
_.each(redisBeacon.broadcast, function(e) {
|
||||
// grab the corresponding subscription
|
||||
var subscription = _.find(subscriptions, function(s) { return s.event === e });
|
||||
|
||||
if(!subscription)
|
||||
util.die('Gekko does not know this event:' + e);
|
||||
|
||||
var channel = redisBeacon.channelPrefix + subscription.event
|
||||
|
||||
proto[subscription.handler] = function(message, cb) {
|
||||
if(!_.isFunction(cb))
|
||||
cb = _.noop;
|
||||
|
||||
this.emit(channel, {
|
||||
market: this.market,
|
||||
data: message
|
||||
}, cb);
|
||||
};
|
||||
|
||||
}, this)
|
||||
|
||||
Actor.prototype = proto;
|
||||
|
||||
Actor.prototype.init = function(done) {
|
||||
this.client = redis.createClient(redisBeacon.port, redisBeacon.host);
|
||||
this.client.on('ready', _.once(done));
|
||||
}
|
||||
|
||||
Actor.prototype.emit = function(channel, message) {
|
||||
log.debug('Going to publish to redis channel:', channel);
|
||||
|
||||
var data = JSON.stringify(message);
|
||||
this.client.publish(channel, data);
|
||||
}
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,96 @@
|
||||
const WebClient = require('@slack/client').WebClient;
|
||||
const _ = require('lodash');
|
||||
const log = require('../core/log.js');
|
||||
const util = require('../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const slackConfig = config.slack;
|
||||
|
||||
const Slack = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.slack;
|
||||
this.price = 'N/A';
|
||||
|
||||
this.done = done;
|
||||
this.setup();
|
||||
};
|
||||
|
||||
Slack.prototype.setup = function(done) {
|
||||
this.slack = new WebClient(slackConfig.token);
|
||||
|
||||
const setupSlack = function(error, result) {
|
||||
if(slackConfig.sendMessageOnStart){
|
||||
const body = this.createResponse("#439FE0","Gekko started!") ;
|
||||
this.send(body);
|
||||
}else{
|
||||
log.debug('Skipping Send message on startup')
|
||||
}
|
||||
};
|
||||
setupSlack.call(this)
|
||||
};
|
||||
|
||||
Slack.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Slack.prototype.processAdvice = function(advice) {
|
||||
if (advice.recommendation == 'soft' && slackConfig.muteSoft) return;
|
||||
|
||||
const color = advice.recommendation === "long" ? "good" : (advice.recommendation === "short" ? "danger" : "warning");
|
||||
const body = this.createResponse(color, "There is a new trend! The advice is to go `" + advice.recommendation + "`! Current price is `" + this.price + "`");
|
||||
|
||||
this.send(body);
|
||||
};
|
||||
|
||||
Slack.prototype.processStratNotification = function({ content }) {
|
||||
const body = this.createResponse('#909399', content);
|
||||
this.send(body);
|
||||
}
|
||||
|
||||
Slack.prototype.send = function(content, done) {
|
||||
this.slack.chat.postMessage(slackConfig.channel, "", content, (error, response) => {
|
||||
if (error || !response) {
|
||||
log.error('Slack ERROR:', error);
|
||||
} else {
|
||||
log.info('Slack Message Sent');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Slack.prototype.checkResults = function(error) {
|
||||
if (error) {
|
||||
log.warn('error sending slack', error);
|
||||
} else {
|
||||
log.info('Send advice via slack.');
|
||||
}
|
||||
};
|
||||
|
||||
Slack.prototype.createResponse = function(color, message) {
|
||||
const template = {
|
||||
"username": this.createUserName(),
|
||||
"icon_url": this.createIconUrl(),
|
||||
"attachments": [
|
||||
{
|
||||
"fallback": "",
|
||||
"color": color,
|
||||
"text": message,
|
||||
"mrkdwn_in": ["text"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
Slack.prototype.createUserName = function() {
|
||||
return config.watch.exchange[0].toUpperCase() + config.watch.exchange.slice(1) + " - " + config.watch.currency + "/" + config.watch.asset;
|
||||
};
|
||||
|
||||
Slack.prototype.createIconUrl = function() {
|
||||
const asset = config.watch.asset === "XBT" ? "btc" :config.watch.asset.toLowerCase();
|
||||
return "https://github.com/cjdowner/cryptocurrency-icons/raw/master/128/icon/" + asset + ".png";
|
||||
};
|
||||
|
||||
module.exports = Slack;
|
||||
@@ -0,0 +1,58 @@
|
||||
var _ = require('lodash');
|
||||
var fs = require('fs');
|
||||
|
||||
var util = require('../../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var dirs = util.dirs();
|
||||
|
||||
var adapter = config.sqlite;
|
||||
|
||||
// verify the correct dependencies are installed
|
||||
var pluginHelper = require(dirs.core + 'pluginUtil');
|
||||
var pluginMock = {
|
||||
slug: 'sqlite adapter',
|
||||
dependencies: adapter.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 = dirs.gekko + adapter.dataDirectory;
|
||||
|
||||
var fullPath = [dir, dbName].join('/');
|
||||
|
||||
var mode = util.gekkoMode();
|
||||
if (mode === 'realtime' || mode === 'importer') {
|
||||
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}.`
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initDB: () => {
|
||||
var journalMode = config.sqlite.journalMode || 'PERSIST';
|
||||
var syncMode = journalMode === 'WAL' ? 'NORMAL' : 'FULL';
|
||||
|
||||
var db = new sqlite3.Database(fullPath);
|
||||
db.run('PRAGMA synchronous = ' + syncMode);
|
||||
db.run('PRAGMA journal_mode = ' + journalMode);
|
||||
db.configure('busyTimeout', 10000);
|
||||
return db;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('../../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var log = require(util.dirs().core + 'log');
|
||||
|
||||
var sqlite = require('./handle');
|
||||
var sqliteUtil = require('./util');
|
||||
|
||||
var Reader = function() {
|
||||
_.bindAll(this);
|
||||
this.db = sqlite.initDB(true);
|
||||
}
|
||||
|
||||
|
||||
// returns the most recent window complete candle
|
||||
// windows within `from` and `to`
|
||||
Reader.prototype.mostRecentWindow = function(from, to, next) {
|
||||
to = to.unix();
|
||||
from = from.unix();
|
||||
|
||||
var maxAmount = to - from + 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) {
|
||||
|
||||
// bail out if the table does not exist
|
||||
if(err.message.split(':')[1] === ' no such table')
|
||||
return next(false);
|
||||
|
||||
log.error(err);
|
||||
return util.die('DB error while reading mostRecentWindow');
|
||||
}
|
||||
|
||||
// no candles are available
|
||||
if(rows.length === 0) {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
if(rows.length === maxAmount) {
|
||||
|
||||
// full history is available!
|
||||
|
||||
return next({
|
||||
from: from,
|
||||
to: to
|
||||
});
|
||||
}
|
||||
|
||||
// we have at least one gap, figure out where
|
||||
var mostRecent = _.first(rows).start;
|
||||
|
||||
var gapIndex = _.findIndex(rows, function(r, i) {
|
||||
return r.start !== mostRecent - i * 60;
|
||||
});
|
||||
|
||||
// if there was no gap in the records, but
|
||||
// there were not enough records.
|
||||
if(gapIndex === -1) {
|
||||
var leastRecent = _.last(rows).start;
|
||||
return next({
|
||||
from: leastRecent,
|
||||
to: mostRecent
|
||||
});
|
||||
}
|
||||
|
||||
// else return mostRecent and the
|
||||
// the minute before the gap
|
||||
return next({
|
||||
from: rows[ gapIndex - 1 ].start,
|
||||
to: mostRecent
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
Reader.prototype.tableExists = function(name, next) {
|
||||
|
||||
this.db.all(`
|
||||
SELECT name FROM sqlite_master WHERE type='table' AND name='${sqliteUtil.table(name)}';
|
||||
`, function(err, rows) {
|
||||
if(err) {
|
||||
console.error(err);
|
||||
return util.die('DB error at `get`');
|
||||
}
|
||||
|
||||
next(null, rows.length === 1);
|
||||
});
|
||||
}
|
||||
|
||||
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.close();
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
module.exports = Reader;
|
||||
@@ -0,0 +1,64 @@
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const fs = require('fs');
|
||||
|
||||
const util = require('../../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const dirs = util.dirs();
|
||||
|
||||
const sqlite3 = require('sqlite3');
|
||||
|
||||
// todo: rewrite with generators or async/await..
|
||||
module.exports = done => {
|
||||
const dbDirectory = dirs.gekko + config.sqlite.dataDirectory
|
||||
|
||||
if(!fs.existsSync(dbDirectory))
|
||||
return done(null, []);
|
||||
|
||||
const files = fs.readdirSync(dbDirectory);
|
||||
|
||||
const dbs = files
|
||||
.filter(f => {
|
||||
let parts = f.split('.');
|
||||
if(_.last(parts) === 'db')
|
||||
return true;
|
||||
})
|
||||
|
||||
if(!_.size(dbs))
|
||||
return done(null, []);
|
||||
|
||||
let markets = [];
|
||||
|
||||
async.each(dbs, (db, next) => {
|
||||
|
||||
const exchange = _.first(db.split('_'));
|
||||
const handle = new sqlite3.Database(dbDirectory + '/' + db, sqlite3.OPEN_READONLY, err => {
|
||||
if(err)
|
||||
return next(err);
|
||||
|
||||
handle.all(`SELECT name FROM sqlite_master WHERE type='table'`, (err, tables) => {
|
||||
if(err)
|
||||
return next(err);
|
||||
|
||||
_.each(tables, table => {
|
||||
let parts = table.name.split('_');
|
||||
let first = parts.shift();
|
||||
if(first === 'candles')
|
||||
markets.push({
|
||||
exchange: exchange,
|
||||
currency: _.first(parts),
|
||||
asset: _.last(parts)
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
// got all tables!
|
||||
err => {
|
||||
done(err, markets);
|
||||
});
|
||||
}
|
||||
@@ -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.sqlite.dataDirectory
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
settings: settings,
|
||||
table: function(name) {
|
||||
return [name, settings.pair.join('_')].join('_');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
var _ = require('lodash');
|
||||
var config = require('../../core/util.js').getConfig();
|
||||
|
||||
var sqlite = require('./handle');
|
||||
var sqliteUtil = require('./util');
|
||||
var util = require('../../core/util');
|
||||
var log = require('../../core/log');
|
||||
|
||||
var Store = function(done, pluginMeta) {
|
||||
_.bindAll(this);
|
||||
this.done = done;
|
||||
|
||||
this.db = sqlite.initDB(false);
|
||||
this.db.serialize(this.upsertTables);
|
||||
|
||||
this.cache = [];
|
||||
this.buffered = util.gekkoMode() === "importer";
|
||||
}
|
||||
|
||||
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 INTEGER 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;
|
||||
|
||||
const transaction = () => {
|
||||
this.db.run("BEGIN TRANSACTION");
|
||||
|
||||
var stmt = this.db.prepare(`
|
||||
INSERT OR IGNORE INTO ${sqliteUtil.table('candles')}
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
`, function(err, rows) {
|
||||
if(err) {
|
||||
log.error(err);
|
||||
return util.die('DB error at INSERT: '+ err);
|
||||
}
|
||||
});
|
||||
|
||||
_.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.db.run("COMMIT");
|
||||
// TEMP: should fix https://forum.gekko.wizb.it/thread-57279-post-59194.html#pid59194
|
||||
this.db.run("pragma wal_checkpoint;");
|
||||
|
||||
this.cache = [];
|
||||
}
|
||||
|
||||
this.db.serialize(transaction);
|
||||
}
|
||||
|
||||
var processCandle = function(candle, done) {
|
||||
this.cache.push(candle);
|
||||
if (!this.buffered || this.cache.length > 1000)
|
||||
this.writeCandles();
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
var finalize = function(done) {
|
||||
this.writeCandles();
|
||||
this.db.close(() => { done(); });
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
if(config.candleWriter.enabled) {
|
||||
Store.prototype.processCandle = processCandle;
|
||||
Store.prototype.finalize = finalize;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -0,0 +1,197 @@
|
||||
const log = require('../core/log');
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const config = require('../core/util').getConfig();
|
||||
const telegrambot = config.telegrambot;
|
||||
const emitTrades = telegrambot.emitTrades;
|
||||
const utc = moment.utc;
|
||||
const telegram = require("node-telegram-bot-api");
|
||||
|
||||
const Actor = function() {
|
||||
_.bindAll(this);
|
||||
|
||||
this.advice = null;
|
||||
this.adviceTime = utc();
|
||||
|
||||
this.price = 'Dont know yet :(';
|
||||
this.priceTime = utc();
|
||||
|
||||
this.commands = {
|
||||
'/start': 'emitStart',
|
||||
'/advice': 'emitAdvice',
|
||||
'/subscribe': 'emitSubscribe',
|
||||
'/unsubscribe': 'emitUnSubscribe',
|
||||
'/price': 'emitPrice',
|
||||
'/help': 'emitHelp'
|
||||
};
|
||||
if (telegrambot.donate) {
|
||||
this.commands['/donate'] = 'emitDonate';
|
||||
}
|
||||
this.rawCommands = _.keys(this.commands);
|
||||
this.chatId = null;
|
||||
this.subscribers = [];
|
||||
this.bot = new telegram(telegrambot.token, { polling: true });
|
||||
this.bot.onText(/(.+)/, this.verifyQuestion);
|
||||
};
|
||||
|
||||
Actor.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
this.priceTime = candle.start;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Actor.prototype.processAdvice = function(advice) {
|
||||
if (advice.recommendation === 'soft') return;
|
||||
this.advice = advice.recommendation;
|
||||
this.adviceTime = utc();
|
||||
this.advicePrice = this.price;
|
||||
this.subscribers.forEach(this.emitAdvice, this);
|
||||
};
|
||||
|
||||
if(emitTrades) {
|
||||
Actor.prototype.processTradeInitiated = function (tradeInitiated) {
|
||||
var message = 'Trade initiated. ID: ' + tradeInitiated.id +
|
||||
'\nAction: ' + tradeInitiated.action + '\nPortfolio: ' +
|
||||
tradeInitiated.portfolio + '\nBalance: ' + tradeInitiated.balance;
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
}
|
||||
|
||||
Actor.prototype.processTradeCancelled = function (tradeCancelled) {
|
||||
var message = 'Trade cancelled. ID: ' + tradeCancelled.id;
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
}
|
||||
|
||||
Actor.prototype.processTradeAborted = function (tradeAborted) {
|
||||
var message = 'Trade aborted. ID: ' + tradeAborted.id +
|
||||
'\nNot creating order! Reason: ' + tradeAborted.reason;
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
}
|
||||
|
||||
Actor.prototype.processTradeErrored = function (tradeErrored) {
|
||||
var message = 'Trade errored. ID: ' + tradeErrored.id +
|
||||
'\nReason: ' + tradeErrored.reason;
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
}
|
||||
|
||||
Actor.prototype.processTradeCompleted = function (tradeCompleted) {
|
||||
var message = 'Trade completed. ID: ' + tradeCompleted.id +
|
||||
'\nAction: ' + tradeCompleted.action +
|
||||
'\nPrice: ' + tradeCompleted.price +
|
||||
'\nAmount: ' + tradeCompleted.amount +
|
||||
'\nCost: ' + tradeCompleted.cost +
|
||||
'\nPortfolio: ' + tradeCompleted.portfolio +
|
||||
'\nBalance: ' + tradeCompleted.balance +
|
||||
'\nFee percent: ' + tradeCompleted.feePercent +
|
||||
'\nEffective price: ' + tradeCompleted.effectivePrice;
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
}
|
||||
}
|
||||
|
||||
Actor.prototype.verifyQuestion = function(msg, text) {
|
||||
this.chatId = msg.chat.id;
|
||||
if (text[1].toLowerCase() in this.commands) {
|
||||
this[this.commands[text[1].toLowerCase()]]();
|
||||
} else {
|
||||
this.emitHelp();
|
||||
}
|
||||
};
|
||||
|
||||
Actor.prototype.emitStart = function() {
|
||||
this.bot.sendMessage(this.chatId, 'Hello! How can I help you?');
|
||||
};
|
||||
|
||||
Actor.prototype.emitSubscribe = function() {
|
||||
if (this.subscribers.indexOf(this.chatId) === -1) {
|
||||
this.subscribers.push(this.chatId);
|
||||
this.bot.sendMessage(this.chatId, `Success! Got ${this.subscribers.length} subscribers.`);
|
||||
} else {
|
||||
this.bot.sendMessage(this.chatId, "You are already subscribed.");
|
||||
}
|
||||
};
|
||||
|
||||
Actor.prototype.emitUnSubscribe = function() {
|
||||
if (this.subscribers.indexOf(this.chatId) > -1) {
|
||||
this.subscribers.splice(this.subscribers.indexOf(this.chatId), 1);
|
||||
this.bot.sendMessage(this.chatId, "Success!");
|
||||
} else {
|
||||
this.bot.sendMessage(this.chatId, "You are not subscribed.");
|
||||
}
|
||||
};
|
||||
|
||||
Actor.prototype.emitAdvice = function(chatId) {
|
||||
let message = [
|
||||
'Advice for ',
|
||||
config.watch.exchange,
|
||||
' ',
|
||||
config.watch.currency,
|
||||
'/',
|
||||
config.watch.asset,
|
||||
' using ',
|
||||
config.tradingAdvisor.method,
|
||||
' at ',
|
||||
config.tradingAdvisor.candleSize,
|
||||
' minute candles, is:\n',
|
||||
].join('');
|
||||
if (this.advice) {
|
||||
message += this.advice +
|
||||
' ' +
|
||||
config.watch.asset +
|
||||
' ' +
|
||||
this.advicePrice +
|
||||
' (' +
|
||||
this.adviceTime.fromNow() +
|
||||
')';
|
||||
} else {
|
||||
message += 'None'
|
||||
}
|
||||
|
||||
if (chatId) {
|
||||
this.bot.sendMessage(chatId, message);
|
||||
} else {
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
}
|
||||
};
|
||||
|
||||
// sent price over to the last chat
|
||||
Actor.prototype.emitPrice = function() {
|
||||
const 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.bot.sendMessage(this.chatId, message);
|
||||
};
|
||||
|
||||
Actor.prototype.emitDonate = function() {
|
||||
this.bot.sendMessage(this.chatId, telegrambot.donate);
|
||||
};
|
||||
|
||||
Actor.prototype.emitHelp = function() {
|
||||
let message = _.reduce(
|
||||
this.rawCommands,
|
||||
function(message, command) {
|
||||
return message + ' ' + command + ',';
|
||||
},
|
||||
'Possible commands are:'
|
||||
);
|
||||
message = message.substr(0, _.size(message) - 1) + '.';
|
||||
this.bot.sendMessage(this.chatId, message);
|
||||
};
|
||||
|
||||
Actor.prototype.logError = function(message) {
|
||||
log.error('Telegram ERROR:', message);
|
||||
};
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,412 @@
|
||||
const _ = require('lodash');
|
||||
const util = require('../../core/util.js');
|
||||
const config = util.getConfig();
|
||||
const dirs = util.dirs();
|
||||
const moment = require('moment');
|
||||
|
||||
const log = require(dirs.core + 'log');
|
||||
const Broker = require(dirs.broker + '/gekkoBroker');
|
||||
|
||||
require(dirs.gekko + '/exchange/dependencyCheck');
|
||||
|
||||
const Trader = function(next) {
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
this.brokerConfig = {
|
||||
...config.trader,
|
||||
...config.watch,
|
||||
private: true
|
||||
}
|
||||
|
||||
this.propogatedTrades = 0;
|
||||
this.propogatedTriggers = 0;
|
||||
|
||||
try {
|
||||
this.broker = new Broker(this.brokerConfig);
|
||||
} catch(e) {
|
||||
util.die(e.message);
|
||||
}
|
||||
|
||||
if(!this.broker.capabilities.gekkoBroker) {
|
||||
util.die('This exchange is not yet supported');
|
||||
}
|
||||
|
||||
this.sync(() => {
|
||||
log.info('\t', 'Portfolio:');
|
||||
log.info('\t\t', this.portfolio.currency, this.brokerConfig.currency);
|
||||
log.info('\t\t', this.portfolio.asset, this.brokerConfig.asset);
|
||||
log.info('\t', 'Balance:');
|
||||
log.info('\t\t', this.balance, this.brokerConfig.currency);
|
||||
log.info('\t', 'Exposed:');
|
||||
log.info('\t\t',
|
||||
this.exposed ? 'yes' : 'no',
|
||||
`(${(this.exposure * 100).toFixed(2)}%)`
|
||||
);
|
||||
next();
|
||||
});
|
||||
|
||||
this.cancellingOrder = false;
|
||||
this.sendInitialPortfolio = false;
|
||||
|
||||
setInterval(this.sync, 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
// teach our trader events
|
||||
util.makeEventEmitter(Trader);
|
||||
|
||||
Trader.prototype.sync = function(next) {
|
||||
log.debug('syncing private data');
|
||||
this.broker.syncPrivateData(() => {
|
||||
if(!this.price) {
|
||||
this.price = this.broker.ticker.bid;
|
||||
}
|
||||
|
||||
const oldPortfolio = this.portfolio;
|
||||
|
||||
this.setPortfolio();
|
||||
this.setBalance();
|
||||
|
||||
if(this.sendInitialPortfolio && !_.isEqual(oldPortfolio, this.portfolio)) {
|
||||
this.relayPortfolioChange();
|
||||
}
|
||||
|
||||
// balance is relayed every minute
|
||||
// no need to do it here.
|
||||
|
||||
if(next) {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Trader.prototype.relayPortfolioChange = function() {
|
||||
this.deferredEmit('portfolioChange', {
|
||||
asset: this.portfolio.asset,
|
||||
currency: this.portfolio.currency
|
||||
});
|
||||
}
|
||||
|
||||
Trader.prototype.relayPortfolioValueChange = function() {
|
||||
this.deferredEmit('portfolioValueChange', {
|
||||
balance: this.balance
|
||||
});
|
||||
}
|
||||
|
||||
Trader.prototype.setPortfolio = function() {
|
||||
this.portfolio = {
|
||||
currency: _.find(
|
||||
this.broker.portfolio.balances,
|
||||
b => b.name === this.brokerConfig.currency
|
||||
).amount,
|
||||
asset: _.find(
|
||||
this.broker.portfolio.balances,
|
||||
b => b.name === this.brokerConfig.asset
|
||||
).amount
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.setBalance = function() {
|
||||
this.balance = this.portfolio.currency + this.portfolio.asset * this.price;
|
||||
this.exposure = (this.portfolio.asset * this.price) / this.balance;
|
||||
// if more than 10% of balance is in asset we are exposed
|
||||
this.exposed = this.exposure > 0.1;
|
||||
}
|
||||
|
||||
Trader.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
const previousBalance = this.balance;
|
||||
this.setPortfolio();
|
||||
this.setBalance();
|
||||
|
||||
if(!this.sendInitialPortfolio) {
|
||||
this.sendInitialPortfolio = true;
|
||||
this.deferredEmit('portfolioChange', {
|
||||
asset: this.portfolio.asset,
|
||||
currency: this.portfolio.currency
|
||||
});
|
||||
}
|
||||
|
||||
if(this.balance !== previousBalance) {
|
||||
// this can happen because:
|
||||
// A) the price moved and we have > 0 asset
|
||||
// B) portfolio got changed
|
||||
this.relayPortfolioValueChange();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
Trader.prototype.processAdvice = function(advice) {
|
||||
let direction;
|
||||
|
||||
if(advice.recommendation === 'long') {
|
||||
direction = 'buy';
|
||||
} else if(advice.recommendation === 'short') {
|
||||
direction = 'sell';
|
||||
} else {
|
||||
log.error('ignoring advice in unknown direction');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = 'trade-' + (++this.propogatedTrades);
|
||||
|
||||
if(this.order) {
|
||||
if(this.order.side === direction) {
|
||||
return log.info('ignoring advice: already in the process to', direction);
|
||||
}
|
||||
|
||||
if(this.cancellingOrder) {
|
||||
return log.info('ignoring advice: already cancelling previous', this.order.side, 'order');
|
||||
}
|
||||
|
||||
log.info('Received advice to', direction, 'however Gekko is already in the process to', this.order.side);
|
||||
log.info('Canceling', this.order.side, 'order first');
|
||||
return this.cancelOrder(id, advice, () => this.processAdvice(advice));
|
||||
}
|
||||
|
||||
let amount;
|
||||
|
||||
if(direction === 'buy') {
|
||||
|
||||
if(this.exposed) {
|
||||
log.info('NOT buying, already exposed');
|
||||
return this.deferredEmit('tradeAborted', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
action: direction,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.balance,
|
||||
reason: "Portfolio already in position."
|
||||
});
|
||||
}
|
||||
|
||||
amount = this.portfolio.currency / this.price * 0.95;
|
||||
|
||||
log.info(
|
||||
'Trader',
|
||||
'Received advice to go long.',
|
||||
'Buying ', this.brokerConfig.asset
|
||||
);
|
||||
|
||||
} else if(direction === 'sell') {
|
||||
|
||||
if(!this.exposed) {
|
||||
log.info('NOT selling, already no exposure');
|
||||
return this.deferredEmit('tradeAborted', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
action: direction,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.balance,
|
||||
reason: "Portfolio already in position."
|
||||
});
|
||||
}
|
||||
|
||||
// clean up potential old stop trigger
|
||||
if(this.activeStopTrigger) {
|
||||
this.deferredEmit('triggerAborted', {
|
||||
id: this.activeStopTrigger.id,
|
||||
date: advice.date
|
||||
});
|
||||
|
||||
this.activeStopTrigger.instance.cancel();
|
||||
|
||||
delete this.activeStopTrigger;
|
||||
}
|
||||
|
||||
amount = this.portfolio.asset;
|
||||
|
||||
log.info(
|
||||
'Trader',
|
||||
'Received advice to go short.',
|
||||
'Selling ', this.brokerConfig.asset
|
||||
);
|
||||
}
|
||||
|
||||
this.createOrder(direction, amount, advice, id);
|
||||
}
|
||||
|
||||
Trader.prototype.createOrder = function(side, amount, advice, id) {
|
||||
const type = 'sticky';
|
||||
|
||||
// NOTE: this is the best check we can do at this point
|
||||
// with the best price we have. The order won't be actually
|
||||
// created with this.price, but it should be close enough to
|
||||
// catch non standard errors (lot size, price filter) on
|
||||
// exchanges that have them.
|
||||
const check = this.broker.isValidOrder(amount, this.price);
|
||||
|
||||
if(!check.valid) {
|
||||
log.warn('NOT creating order! Reason:', check.reason);
|
||||
return this.deferredEmit('tradeAborted', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
action: side,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.balance,
|
||||
reason: check.reason
|
||||
});
|
||||
}
|
||||
|
||||
log.debug('Creating order to', side, amount, this.brokerConfig.asset);
|
||||
|
||||
this.deferredEmit('tradeInitiated', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
action: side,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.balance
|
||||
});
|
||||
|
||||
this.order = this.broker.createOrder(type, side, amount);
|
||||
|
||||
this.order.on('fill', f => log.info('[ORDER] partial', side, 'fill, total filled:', f));
|
||||
this.order.on('statusChange', s => log.debug('[ORDER] statusChange:', s));
|
||||
|
||||
this.order.on('error', e => {
|
||||
log.error('[ORDER] Gekko received error from GB:', e.message);
|
||||
log.debug(e);
|
||||
this.order = null;
|
||||
this.cancellingOrder = false;
|
||||
|
||||
this.deferredEmit('tradeErrored', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
date: moment(),
|
||||
reason: e.message
|
||||
});
|
||||
|
||||
});
|
||||
this.order.on('completed', () => {
|
||||
this.order.createSummary((err, summary) => {
|
||||
if(!err && !summary) {
|
||||
err = new Error('GB returned an empty summary.')
|
||||
}
|
||||
|
||||
if(err) {
|
||||
log.error('Error while creating summary:', err);
|
||||
return this.deferredEmit('tradeErrored', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
date: moment(),
|
||||
reason: err.message
|
||||
});
|
||||
}
|
||||
|
||||
log.info('[ORDER] summary:', summary);
|
||||
this.order = null;
|
||||
this.sync(() => {
|
||||
|
||||
let cost;
|
||||
if(_.isNumber(summary.feePercent)) {
|
||||
cost = summary.feePercent / 100 * summary.amount * summary.price;
|
||||
}
|
||||
|
||||
let effectivePrice;
|
||||
if(_.isNumber(summary.feePercent)) {
|
||||
if(side === 'buy') {
|
||||
effectivePrice = summary.price * (1 + summary.feePercent / 100);
|
||||
} else {
|
||||
effectivePrice = summary.price * (1 - summary.feePercent / 100);
|
||||
}
|
||||
} else {
|
||||
log.warn('WARNING: exchange did not provide fee information, assuming no fees..');
|
||||
effectivePrice = summary.price;
|
||||
}
|
||||
|
||||
this.deferredEmit('tradeCompleted', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
action: summary.side,
|
||||
cost,
|
||||
amount: summary.amount,
|
||||
price: summary.price,
|
||||
portfolio: this.portfolio,
|
||||
balance: this.balance,
|
||||
date: summary.date,
|
||||
feePercent: summary.feePercent,
|
||||
effectivePrice
|
||||
});
|
||||
|
||||
if(
|
||||
side === 'buy' &&
|
||||
advice.trigger &&
|
||||
advice.trigger.type === 'trailingStop'
|
||||
) {
|
||||
const trigger = advice.trigger;
|
||||
const triggerId = 'trigger-' + (++this.propogatedTriggers);
|
||||
|
||||
this.deferredEmit('triggerCreated', {
|
||||
id: triggerId,
|
||||
at: advice.date,
|
||||
type: 'trailingStop',
|
||||
properties: {
|
||||
trail: trigger.trailValue,
|
||||
initialPrice: summary.price,
|
||||
}
|
||||
});
|
||||
|
||||
log.info(`Creating trailingStop trigger "${triggerId}"! Properties:`);
|
||||
log.info(`\tInitial price: ${summary.price}`);
|
||||
log.info(`\tTrail of: ${trigger.trailValue}`);
|
||||
|
||||
this.activeStopTrigger = {
|
||||
id: triggerId,
|
||||
adviceId: advice.id,
|
||||
instance: this.broker.createTrigger({
|
||||
type: 'trailingStop',
|
||||
onTrigger: this.onStopTrigger,
|
||||
props: {
|
||||
trail: trigger.trailValue,
|
||||
initialPrice: summary.price,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Trader.prototype.onStopTrigger = function(price) {
|
||||
log.info(`TrailingStop trigger "${this.activeStopTrigger.id}" fired! Observed price was ${price}`);
|
||||
|
||||
this.deferredEmit('triggerFired', {
|
||||
id: this.activeStopTrigger.id,
|
||||
date: moment()
|
||||
});
|
||||
|
||||
const adviceMock = {
|
||||
recommendation: 'short',
|
||||
id: this.activeStopTrigger.adviceId
|
||||
}
|
||||
|
||||
delete this.activeStopTrigger;
|
||||
|
||||
this.processAdvice(adviceMock);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(id, advice, next) {
|
||||
|
||||
if(!this.order) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.cancellingOrder = true;
|
||||
|
||||
this.order.removeAllListeners();
|
||||
this.order.cancel();
|
||||
this.order.once('completed', () => {
|
||||
this.order = null;
|
||||
this.cancellingOrder = false;
|
||||
this.deferredEmit('tradeCancelled', {
|
||||
id,
|
||||
adviceId: advice.id,
|
||||
date: moment()
|
||||
});
|
||||
this.sync(next);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,150 @@
|
||||
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 talib = require(dirs.core + 'talib');
|
||||
const tulind = require(dirs.core + 'tulind');
|
||||
|
||||
const allowedTalibIndicators = _.keys(talib);
|
||||
const allowedTulipIndicators = _.keys(tulind);
|
||||
|
||||
const AsyncIndicatorRunner = function() {
|
||||
this.talibIndicators = {};
|
||||
this.tulipIndicators = {};
|
||||
|
||||
this.candleProps = {
|
||||
open: [],
|
||||
high: [],
|
||||
low: [],
|
||||
close: [],
|
||||
volume: []
|
||||
};
|
||||
|
||||
this.candlePropsCacheSize = 1000;
|
||||
|
||||
this.inflight = false;
|
||||
this.backlog = [];
|
||||
this.age = 0;
|
||||
}
|
||||
|
||||
AsyncIndicatorRunner.prototype.processCandle = function(candle, next) {
|
||||
if(this.inflight) {
|
||||
return this.backlog.push({candle, next});
|
||||
}
|
||||
|
||||
this.age++;
|
||||
this.inflight = true;
|
||||
|
||||
this.candleProps.open.push(candle.open);
|
||||
this.candleProps.high.push(candle.high);
|
||||
this.candleProps.low.push(candle.low);
|
||||
this.candleProps.close.push(candle.close);
|
||||
this.candleProps.volume.push(candle.volume);
|
||||
|
||||
if(this.age > this.candlePropsCacheSize) {
|
||||
this.candleProps.open.shift();
|
||||
this.candleProps.high.shift();
|
||||
this.candleProps.low.shift();
|
||||
this.candleProps.close.shift();
|
||||
this.candleProps.volume.shift();
|
||||
}
|
||||
|
||||
this.calculateIndicators(next);
|
||||
}
|
||||
|
||||
AsyncIndicatorRunner.prototype.calculateIndicators = function(next) {
|
||||
const done = _.after(
|
||||
_.size(this.talibIndicators) + _.size(this.tulipIndicators),
|
||||
this.handlePostFlight(next)
|
||||
);
|
||||
|
||||
// handle result from talib
|
||||
const talibResultHander = name => (err, result) => {
|
||||
if(err)
|
||||
util.die('TALIB ERROR:', err);
|
||||
|
||||
this.talibIndicators[name].result = _.mapValues(result, v => _.last(v));
|
||||
done();
|
||||
}
|
||||
|
||||
// handle result from talib
|
||||
_.each(
|
||||
this.talibIndicators,
|
||||
(indicator, name) => indicator.run(
|
||||
this.candleProps,
|
||||
talibResultHander(name)
|
||||
)
|
||||
);
|
||||
|
||||
// handle result from tulip
|
||||
var tulindResultHander = name => (err, result) => {
|
||||
if(err)
|
||||
util.die('TULIP ERROR:', err);
|
||||
|
||||
this.tulipIndicators[name].result = _.mapValues(result, v => _.last(v));
|
||||
done();
|
||||
}
|
||||
|
||||
// handle result from tulip indicators
|
||||
_.each(
|
||||
this.tulipIndicators,
|
||||
(indicator, name) => indicator.run(
|
||||
this.candleProps,
|
||||
tulindResultHander(name)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
AsyncIndicatorRunner.prototype.handlePostFlight = function(next) {
|
||||
return () => {
|
||||
next();
|
||||
this.inflight = false;
|
||||
|
||||
if(this.backlog.length) {
|
||||
const { candle, next } = this.backlog.shift();
|
||||
this.processCandle(candle, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncIndicatorRunner.prototype.addTalibIndicator = function(name, type, parameters) {
|
||||
if(!talib)
|
||||
util.die('Talib is not enabled');
|
||||
|
||||
if(!_.contains(allowedTalibIndicators, type))
|
||||
util.die('I do not know the talib indicator ' + type);
|
||||
|
||||
if(this.setup)
|
||||
util.die('Can only add talib indicators in the init method!');
|
||||
|
||||
var basectx = this;
|
||||
|
||||
this.talibIndicators[name] = {
|
||||
run: talib[type].create(parameters),
|
||||
result: NaN
|
||||
}
|
||||
}
|
||||
|
||||
AsyncIndicatorRunner.prototype.addTulipIndicator = function(name, type, parameters) {
|
||||
if(!tulind) {
|
||||
util.die('Tulip indicators is not enabled');
|
||||
}
|
||||
|
||||
if(!_.contains(allowedTulipIndicators, type))
|
||||
util.die('I do not know the tulip indicator ' + type);
|
||||
|
||||
if(this.setup)
|
||||
util.die('Can only add tulip indicators in the init method!');
|
||||
|
||||
var basectx = this;
|
||||
|
||||
this.tulipIndicators[name] = {
|
||||
run: tulind[type].create(parameters),
|
||||
result: NaN
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AsyncIndicatorRunner;
|
||||
@@ -0,0 +1,334 @@
|
||||
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;
|
||||
@@ -0,0 +1,122 @@
|
||||
var util = require('../../core/util');
|
||||
var _ = require('lodash');
|
||||
var fs = require('fs');
|
||||
var toml = require('toml');
|
||||
|
||||
var config = util.getConfig();
|
||||
var dirs = util.dirs();
|
||||
var log = require(dirs.core + 'log');
|
||||
var CandleBatcher = require(dirs.core + 'candleBatcher');
|
||||
|
||||
var moment = require('moment');
|
||||
var isLeecher = config.market && config.market.type === 'leech';
|
||||
|
||||
var Actor = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.done = done;
|
||||
|
||||
this.batcher = new CandleBatcher(config.tradingAdvisor.candleSize);
|
||||
|
||||
this.strategyName = config.tradingAdvisor.method;
|
||||
|
||||
this.setupStrategy();
|
||||
|
||||
var mode = util.gekkoMode();
|
||||
|
||||
// the stitcher will try to pump in historical data
|
||||
// so that the strat can use this data as a "warmup period"
|
||||
//
|
||||
// the realtime "leech" market won't use the stitcher
|
||||
if(mode === 'realtime' && !isLeecher) {
|
||||
var Stitcher = require(dirs.tools + 'dataStitcher');
|
||||
var stitcher = new Stitcher(this.batcher);
|
||||
stitcher.prepareHistoricalData(done);
|
||||
} else
|
||||
done();
|
||||
}
|
||||
|
||||
Actor.prototype.setupStrategy = function() {
|
||||
|
||||
if(!fs.existsSync(dirs.methods + this.strategyName + '.js'))
|
||||
util.die('Gekko can\'t find the strategy "' + this.strategyName + '"');
|
||||
|
||||
log.info('\t', 'Using the strategy: ' + this.strategyName);
|
||||
|
||||
const strategy = require(dirs.methods + this.strategyName);
|
||||
|
||||
// bind all trading strategy specific functions
|
||||
// to the WrappedStrategy.
|
||||
const WrappedStrategy = require('./baseTradingMethod');
|
||||
|
||||
_.each(strategy, function(fn, name) {
|
||||
WrappedStrategy.prototype[name] = fn;
|
||||
});
|
||||
|
||||
let stratSettings;
|
||||
if(config[this.strategyName]) {
|
||||
stratSettings = config[this.strategyName];
|
||||
}
|
||||
|
||||
this.strategy = new WrappedStrategy(stratSettings);
|
||||
this.strategy
|
||||
.on(
|
||||
'stratWarmupCompleted',
|
||||
e => this.deferredEmit('stratWarmupCompleted', e)
|
||||
)
|
||||
.on('advice', this.relayAdvice)
|
||||
.on(
|
||||
'stratUpdate',
|
||||
e => this.deferredEmit('stratUpdate', e)
|
||||
).on('stratNotification',
|
||||
e => this.deferredEmit('stratNotification', e)
|
||||
)
|
||||
|
||||
this.strategy
|
||||
.on('tradeCompleted', this.processTradeCompleted);
|
||||
|
||||
this.batcher
|
||||
.on('candle', _candle => {
|
||||
const { id, ...candle } = _candle;
|
||||
this.deferredEmit('stratCandle', candle);
|
||||
this.emitStratCandle(candle);
|
||||
});
|
||||
}
|
||||
|
||||
// HANDLERS
|
||||
// process the 1m candles
|
||||
Actor.prototype.processCandle = function(candle, done) {
|
||||
this.candle = candle;
|
||||
const completedBatch = this.batcher.write([candle]);
|
||||
if(completedBatch) {
|
||||
this.next = done;
|
||||
} else {
|
||||
done();
|
||||
this.next = false;
|
||||
}
|
||||
this.batcher.flush();
|
||||
}
|
||||
|
||||
// propogate a custom sized candle to the trading strategy
|
||||
Actor.prototype.emitStratCandle = function(candle) {
|
||||
const next = this.next || _.noop;
|
||||
this.strategy.tick(candle, next);
|
||||
}
|
||||
|
||||
Actor.prototype.processTradeCompleted = function(trade) {
|
||||
this.strategy.processTrade(trade);
|
||||
}
|
||||
|
||||
// pass through shutdown handler
|
||||
Actor.prototype.finish = function(done) {
|
||||
this.strategy.finish(done);
|
||||
}
|
||||
|
||||
// EMITTERS
|
||||
Actor.prototype.relayAdvice = function(advice) {
|
||||
advice.date = this.candle.start.clone().add(1, 'minute');
|
||||
this.deferredEmit('advice', advice);
|
||||
}
|
||||
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,84 @@
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log.js');
|
||||
var util = require('../core/util.js');
|
||||
var config = util.getConfig();
|
||||
var twitterConfig = config.twitter;
|
||||
var TwitterApi = require('twitter');
|
||||
|
||||
require('dotenv').config()
|
||||
|
||||
var Twitter = function(done) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.twitter;
|
||||
this.price = 'N/A';
|
||||
this.done = done;
|
||||
this.setup();
|
||||
|
||||
};
|
||||
|
||||
Twitter.prototype.setup = function(done){
|
||||
var setupTwitter = function (err, result) {
|
||||
this.client = new TwitterApi({
|
||||
consumer_key: twitterConfig.consumer_key,
|
||||
consumer_secret: twitterConfig.consumer_secret,
|
||||
access_token_key: twitterConfig.access_token_key,
|
||||
access_token_secret: twitterConfig.access_token_secret
|
||||
});
|
||||
|
||||
if(twitterConfig.sendMessageOnStart){
|
||||
var exchange = config.watch.exchange;
|
||||
var currency = config.watch.currency;
|
||||
var asset = config.watch.asset;
|
||||
var body = "Watching "
|
||||
+exchange
|
||||
+" "
|
||||
+currency
|
||||
+" "
|
||||
+asset
|
||||
this.mail(body);
|
||||
}else{
|
||||
log.debug('Skipping Send message on startup')
|
||||
}
|
||||
};
|
||||
setupTwitter.call(this)
|
||||
};
|
||||
|
||||
Twitter.prototype.processCandle = function(candle, done) {
|
||||
this.price = candle.close;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
Twitter.prototype.processAdvice = function(advice) {
|
||||
if (advice.recommendation == "soft" && twitterConfig.muteSoft) return;
|
||||
var text = [
|
||||
'New ', config.watch.asset, ' trend. Attempting to ',
|
||||
advice.recommendation == "short" ? "sell" : "buy",
|
||||
' @',
|
||||
this.price,
|
||||
].join('');
|
||||
|
||||
this.mail(text);
|
||||
};
|
||||
|
||||
Twitter.prototype.mail = function(content, done) {
|
||||
log.info("trying to tweet");
|
||||
this.client.post('statuses/update', {status: content}, function(error, tweet, response) {
|
||||
if(error || !response) {
|
||||
log.error('Twitter ERROR:', error)
|
||||
} else if(response && response.active){
|
||||
log.info('Twitter Message Sent')
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Twitter.prototype.checkResults = function(err) {
|
||||
if(err)
|
||||
log.warn('error sending email', err);
|
||||
else
|
||||
log.info('Send advice via email.');
|
||||
};
|
||||
|
||||
|
||||
module.exports = Twitter;
|
||||
@@ -0,0 +1,27 @@
|
||||
var log = require('../core/log');
|
||||
var moment = require('moment');
|
||||
var _ = require('lodash');
|
||||
var Server = require('../web/server.js');
|
||||
|
||||
var Actor = function(next) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.server = new Server();
|
||||
this.server.setup(next);
|
||||
}
|
||||
|
||||
Actor.prototype.init = function(data) {
|
||||
this.server.broadcastHistory(data);
|
||||
};
|
||||
|
||||
Actor.prototype.processCandle = function(candle, next) {
|
||||
this.server.broadcastCandle(candle);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
Actor.prototype.processAdvice = function(advice) {
|
||||
this.server.broadcastAdvice(advice);
|
||||
};
|
||||
|
||||
module.exports = Actor;
|
||||
@@ -0,0 +1,193 @@
|
||||
var log = require('../core/log');
|
||||
var moment = require('moment');
|
||||
var _ = require('lodash');
|
||||
var xmpp = require('node-xmpp-client');
|
||||
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);
|
||||
this.bot.connection.socket.setTimeout(0)
|
||||
this.bot.connection.socket.setKeepAlive(true, 10000)
|
||||
|
||||
}
|
||||
|
||||
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.start;
|
||||
};
|
||||
|
||||
Actor.prototype.processAdvice = function(advice) {
|
||||
if (xmppbot.muteSoft && advice.recommendation === 'soft') return;
|
||||
this.advice = advice.recommendation;
|
||||
this.adviceTime = utc();
|
||||
|
||||
if(xmppbot.emitUpdates)
|
||||
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;
|
||||
Reference in new issue
Block a user