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

413 lines
10 KiB
JavaScript

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;