Skip to content
This repository has been archived by the owner on Feb 16, 2020. It is now read-only.

Introduce Trade Class #1968

Merged
merged 12 commits into from
Apr 1, 2018
36 changes: 18 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

249 changes: 29 additions & 220 deletions plugins/trader/portfolioManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var log = require(dirs.core + 'log');
var async = require('async');
var checker = require(dirs.core + 'exchangeChecker.js');
var moment = require('moment');
var Trade = require('./trade');

var Manager = function(conf) {
_.bindAll(this);
Expand All @@ -34,7 +35,6 @@ var Manager = function(conf) {
this.conf = conf;
this.portfolio = {};
this.fee;
this.action;

this.marketConfig = _.find(this.exchangeMeta.markets, function(p) {
return _.first(p.pair) === conf.currency.toUpperCase() && _.last(p.pair) === conf.asset.toUpperCase();
Expand All @@ -45,13 +45,15 @@ var Manager = function(conf) {
this.asset = conf.asset;
this.keepAsset = 0;

// contains instantiated trade classes
this.currentTrade = false
this.tradeHistory = [];

if(_.isNumber(conf.keepAsset)) {
log.debug('Keep asset is active. Will try to keep at least ' + conf.keepAsset + ' ' + conf.asset);
this.keepAsset = conf.keepAsset;
}

// resets after every order
this.orders = [];
};

// teach our trader events
Expand Down Expand Up @@ -137,195 +139,50 @@ Manager.prototype.setTicker = function(callback) {
Manager.prototype.getFund = function(fund) {
return _.find(this.portfolio, function(f) { return f.name === fund});
};

Manager.prototype.getBalance = function(fund) {
return this.getFund(fund).amount;
};

// This function makes sure the limit order gets submitted
// to the exchange and initiates order registers watchers.
Manager.prototype.trade = function(what, retry) {
// if we are still busy executing the last trade
// cancel that one (and ignore results = assume not filled)
if(!retry && _.size(this.orders))
return this.cancelLastOrder(() => this.trade(what));

this.action = what;

var act = function() {
var amount, price;

if(what === 'BUY') {

amount = this.getBalance(this.currency) / this.ticker.ask;
if(amount > 0){
price = this.ticker.bid;
this.buy(amount, price);
}
} else if(what === 'SELL') {

amount = this.getBalance(this.asset) - this.keepAsset;
if(amount > 0){
price = this.ticker.ask;
this.sell(amount, price);
}
}
};
async.series([
this.setTicker,
this.setPortfolio,
this.setFee
], _.bind(act, this));

};

Manager.prototype.getMinimum = function(price) {
if(this.minimalOrder.unit === 'currency')
return minimum = this.minimalOrder.amount / price;
else
return minimum = this.minimalOrder.amount;
};

// first do a quick check to see whether we can buy
// the asset, if so BUY and keep track of the order
// (amount is in asset quantity)
Manager.prototype.buy = function(amount, price) {
let minimum = 0;
let process = (err, order) => {
// if order to small
if(!order.amount || order.amount < minimum) {
return log.warn(
'Wanted to buy',
this.asset,
'but the amount is too small ',
'(' + parseFloat(amount).toFixed(8) + ' @',
parseFloat(price).toFixed(8),
') at',
this.exchange.name
);
}

log.info(
'Attempting to BUY',
order.amount,
this.asset,
'at',
this.exchange.name,
'price:',
order.price
);

this.exchange.buy(order.amount, order.price, this.noteOrder);
}
Manager.prototype.trade = function(what) {

if (_.has(this.exchange, 'getLotSize')) {
this.exchange.getLotSize('buy', amount, price, _.bind(process));
} else {
minimum = this.getMinimum(price);
process(undefined, { amount: amount, price: price });
}
};
var makeNewTrade = function(){
this.newTrade(what)
}.bind(this)

// first do a quick check to see whether we can sell
// the asset, if so SELL and keep track of the order
// (amount is in asset quantity)
Manager.prototype.sell = function(amount, price) {
let minimum = 0;
let process = (err, order) => {
// if order to small
if (!order.amount || order.amount < minimum) {
return log.warn(
'Wanted to buy',
this.currency,
'but the amount is too small ',
'(' + parseFloat(amount).toFixed(8) + ' @',
parseFloat(price).toFixed(8),
') at',
this.exchange.name
);
// if an active trade is currently happening
if(this.currentTrade && this.currentTrade.isActive){
if(this.currentTrade.action !== what){
// if the action is different, stop the current trade, then start a new one
this.currentTrade.stop(makeNewTrade)
} else{
// do nothing, the trade is already going
}

log.info(
'Attempting to SELL',
order.amount,
this.asset,
'at',
this.exchange.name,
'price:',
order.price
);

this.exchange.sell(order.amount, order.price, this.noteOrder);
}

if (_.has(this.exchange, 'getLotSize')) {
this.exchange.getLotSize('sell', amount, price, _.bind(process));
} else {
minimum = this.getMinimum(price);
process(undefined, { amount: amount, price: price });
makeNewTrade()
}
};

Manager.prototype.noteOrder = function(err, order) {
if(err) {
util.die(err);
// instantiate a new trade object
// TODO - impliment different trade execution types / strategies
// by invoking variable trade subclasses
// - pass the trade a specific amount limited by a currency allowance
Manager.prototype.newTrade = function(what) {
// push the current (asummed to be inactive) trade to the history
if(this.currentTrade){
this.tradeHistory.push(this.currentTrade)
this.currentTrade = false
}

this.orders.push(order);

// If unfilled, cancel and replace order with adjusted price
let cancelDelay = this.conf.orderUpdateDelay || 1;
setTimeout(this.checkOrder, util.minToMs(cancelDelay));
return this.currentTrade = new Trade(this,{action: what})
Copy link
Owner

@askmike askmike Feb 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to stick to OO: can you not pass the parent (portfolioManager) into the child (trade), these circular references are tightly coupled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's not ideal, let's fix this.

Currently there are multiple references to the manager inside the trade class, which would call for some restructuring. This is what we could do:

  • A new exchange object could be created inside the Trade class so it's not referencing it in the Manager, thus removing references to manager.exchange
  • We can pass in the current pair currency/asset handles into the Trade class on creation, and remove manager.asset and manager.currency
  • Currently the following methods are being called by the Trade class, how do you suggest we handle this?
this.manager.setTicker
this.manager.setPortfolio
this.manager.setFee
this.manager.getMinimum
this.manager.convertPortfolio

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.manager.convertPortfolio

This is currently only needed for emitting events upstream, the trade class doesn't need it.

Some other functions won't be needed anymore by the portfolioManager, like setFee and setTicker (we will need this one later again, for the portfolioValueChange event for example). All the other functions will be used differently anyway, so I propose the portfolioManager passes the exchange (this.exchange) to the tradeclass which reimplements what it needs, we can also drop in the portfolioManager what we don't need. What do you think?

Copy link
Contributor Author

@ansonphong ansonphong Mar 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@askmike OK that makes sense, yes let's keep a single Exchange instance, and move the methods into the Trade class. I'll make that happen, test it and update this PR.

};


Manager.prototype.cancelLastOrder = function(done) {
this.exchange.cancelOrder(_.last(this.orders), alreadyFilled => {
if(alreadyFilled)
return this.relayOrder(done);

this.orders = [];
done();
});
}

// check whether the order got fully filled
// if it is not: cancel & instantiate a new order
Manager.prototype.checkOrder = function() {
var handleCheckResult = function(err, filled) {
if(!filled) {
log.info(this.action, 'order was not (fully) filled, cancelling and creating new order');
this.exchange.cancelOrder(_.last(this.orders), _.bind(handleCancelResult, this));

return;
}

log.info(this.action, 'was successfull');

this.relayOrder();
}

var handleCancelResult = function(alreadyFilled) {
if(alreadyFilled)
return;

if(this.exchangeMeta.forceReorderDelay) {
//We need to wait in case a canceled order has already reduced the amount
var wait = 10;
log.debug(`Waiting ${wait} seconds before starting a new trade on ${this.exchangeMeta.name}!`);

setTimeout(
() => this.trade(this.action, true),
+moment.duration(wait, 'seconds')
);
return;
}

this.trade(this.action, true);
}

this.exchange.checkOrder(_.last(this.orders), _.bind(handleCheckResult, this));
}

// convert into the portfolio expected by the performanceAnalyzer
Manager.prototype.convertPortfolio = function(portfolio) {
var asset = _.find(portfolio, a => a.name === this.asset).amount;
Expand All @@ -338,60 +195,12 @@ Manager.prototype.convertPortfolio = function(portfolio) {
}
}

Manager.prototype.relayOrder = function(done) {
// look up all executed orders and relay average.
var relay = (err, res) => {

var price = 0;
var amount = 0;
var date = moment(0);

_.each(res.filter(o => !_.isUndefined(o) && o.amount), order => {
date = _.max([moment(order.date), date]);
price = ((price * amount) + (order.price * order.amount)) / (order.amount + amount);
amount += +order.amount;
});

async.series([
this.setPortfolio,
this.setTicker
], () => {
const portfolio = this.convertPortfolio(this.portfolio);

this.emit('trade', {
date,
price,
portfolio: portfolio,
balance: portfolio.balance,

// NOTE: within the portfolioManager
// this is in uppercase, everywhere else
// (UI, performanceAnalyzer, etc. it is
// lowercase)
action: this.action.toLowerCase()
});

this.orders = [];

if(_.isFunction(done))
done();
});

}

var getOrders = _.map(
this.orders,
order => next => this.exchange.getOrder(order, next)
);

async.series(getOrders, relay);
}

Manager.prototype.logPortfolio = function() {
log.info(this.exchange.name, 'portfolio:');
_.each(this.portfolio, function(fund) {
log.info('\t', fund.name + ':', parseFloat(fund.amount).toFixed(12));
});
};


module.exports = Manager;
Loading