first commit

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

No files matched your search

+36
View File
@@ -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;
+146
View File
@@ -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;
+118
View File
@@ -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;
+132
View File
@@ -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;
+77
View File
@@ -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;
+24
View File
@@ -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;
+20
View File
@@ -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;
+88
View File
@@ -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;
+154
View File
@@ -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;
+68
View File
@@ -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;
+138
View File
@@ -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;
+47
View File
@@ -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;
+120
View File
@@ -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;
+40
View File
@@ -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)
}
+15
View File
@@ -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
};
+115
View File
@@ -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;
+283
View File
@@ -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;
+134
View File
@@ -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;
+114
View File
@@ -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;
+209
View File
@@ -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;
+93
View File
@@ -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);
});
});
});
}
+66
View File
@@ -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';
}
}
+73
View File
@@ -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;
+311
View File
@@ -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;
+85
View File
@@ -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;
+67
View File
@@ -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;
+96
View File
@@ -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;
+58
View File
@@ -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;
}
};
+169
View File
@@ -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;
+64
View File
@@ -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);
});
}
+15
View File
@@ -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('_');
}
}
+128
View File
@@ -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;
+197
View File
@@ -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;
+412
View File
@@ -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;
+334
View File
@@ -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;
+122
View File
@@ -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;
+84
View File
@@ -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;
+27
View File
@@ -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;
+193
View File
@@ -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;