Skip to content

Instantly share code, notes, and snippets.

@bitkominer
Last active January 25, 2018 11:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bitkominer/5db8f424f577ed365a26e48259cd3853 to your computer and use it in GitHub Desktop.
Save bitkominer/5db8f424f577ed365a26e48259cd3853 to your computer and use it in GitHub Desktop.
/*
The portfolio manager is responsible for making sure that
all decisions are turned into orders and make sure these orders
get executed. Besides the orders the manager also keeps track of
the client's portfolio.
NOTE: Execution strategy is limit orders (to not cross the book)
*/
var _ = require('lodash');
var util = require('../../core/util');
var dirs = util.dirs();
var events = require("events");
var log = require(dirs.core + 'log');
var async = require('async');
var checker = require(dirs.core + 'exchangeChecker.js');
var moment = require('moment');
var Manager = function(conf) {
_.bindAll(this);
var error = checker.cantTrade(conf);
if(error)
util.die(error);
this.exchangeMeta = checker.settings(conf);
// create an exchange
var Exchange = require(dirs.exchanges + this.exchangeMeta.slug);
this.exchange = new Exchange(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();
});
this.minimalOrder = this.marketConfig.minimalOrder;
this.currency = conf.currency;
this.asset = conf.asset;
this.keepAsset = 0;
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
util.makeEventEmitter(Manager);
Manager.prototype.init = function(callback) {
log.debug('getting ticker, balance & fee from', this.exchange.name);
var prepare = function() {
this.starting = false;
log.info('trading at', this.exchange.name, 'ACTIVE');
log.info(this.exchange.name, 'trading fee will be:', this.fee * 100 + '%');
this.logPortfolio();
callback();
};
async.series([
this.setTicker,
this.setPortfolio,
this.setFee
], _.bind(prepare, this));
}
Manager.prototype.setPortfolio = function(callback) {
var set = function(err, fullPortfolio) {
if(err)
util.die(err);
// only include the currency/asset of this market
const portfolio = [ this.conf.currency, this.conf.asset ]
.map(name => {
let item = _.find(fullPortfolio, {name});
if(!item) {
log.debug(`Unable to find "${name}" in portfolio provided by exchange, assuming 0.`);
item = {name, amount: 0};
}
return item;
});
if(_.isEmpty(this.portfolio))
this.emit('portfolioUpdate', this.convertPortfolio(portfolio));
this.portfolio = portfolio;
if(_.isFunction(callback))
callback();
}.bind(this);
this.exchange.getPortfolio(set);
};
Manager.prototype.setFee = function(callback) {
var set = function(err, fee) {
this.fee = fee;
if(err)
util.die(err);
if(_.isFunction(callback))
callback();
}.bind(this);
this.exchange.getFee(set);
};
Manager.prototype.setTicker = function(callback) {
var set = function(err, ticker) {
this.ticker = ticker;
if(err)
util.die(err);
if(_.isFunction(callback))
callback();
}.bind(this);
this.exchange.getTicker(set);
};
// return the [fund] based on the data we have in memory
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, amounttobuy, amounttosell, buyprice, sellprice, hiprice, loprice;
if(what === 'BUY') {
amount = this.getBalance(this.currency) / this.ticker.ask;
if(amount > 0){
buyprice = this.ticker.bid - (0.001 * this.ticker.bid);
sellprice = buyprice + (0.005 * buyprice);
hiprice = this.ticker.ask + (0.001 * this.ticker.ask);
if(sellprice < hiprice){
sellprice = hiprice;
}
amounttobuy = 0.00011000;
amounttosell = amounttobuy;
this.buy(amounttobuy, buyprice);
this.sell(amounttosell, sellprice);
}
} else if(what === 'SELL') {
amount = this.getBalance(this.asset) - this.keepAsset;
if(amount > 0){
sellprice = this.ticker.ask + (0.001 * this.ticker.ask) ;
buyprice = sellprice - (0.005 * sellprice);
loprice = this.ticker.bid - (0.001 * this.ticker.bid);
if(buyprice > loprice){
buyprice = loprice;
}
amounttosell = 0.00011000;
amounttobuy = amounttosell;
this.sell(amounttosell, sellprice);
this.buy(amounttobuy, buyprice);
}
}
};
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) {
var minimum = this.getMinimum(price);
// if order to small
if(amount < minimum) {
return log.error(
'Wanted to buy',
this.asset,
'but the amount is too small',
'(' + parseFloat(amount).toFixed(12) + ')',
'at',
this.exchange.name
);
}
log.info(
'Attempting to BUY',
amount,
this.asset,
'at',
this.exchange.name,
'price:',
price
);
this.exchange.buy(amount, price, this.noteOrder);
};
// 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) {
var minimum = this.getMinimum(price);
// if order to small
if(amount < minimum) {
return log.error(
'Wanted to buy',
this.currency,
'but the amount is too small',
'(' + parseFloat(amount).toFixed(12) + ')',
'at',
this.exchange.name
);
}
log.info(
'Attempting to SELL',
amount,
this.asset,
'at',
this.exchange.name,
'price:',
price
);
this.exchange.sell(amount, price, this.noteOrder);
};
Manager.prototype.noteOrder = function(err, order) {
if(err) {
util.die(err);
}
this.orders.push(order);
// if after 1 minute the order is still there
// we cancel and calculate & make a new one
setTimeout(this.checkOrder, util.minToMs(1));
};
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;
var currency = _.find(portfolio, a => a.name === this.currency).amount;
return {
currency,
asset,
balance: currency + (asset * this.ticker.bid)
}
}
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;
@mstfimi
Copy link

mstfimi commented Jan 25, 2018

I get this error while backtesting, can you help me please?

Error: Cannot find module '../../core/util'
at Function.Module._resolveFilename (module.js:538:15)
at Function.Module._load (module.js:468:25)
at Module.require (module.js:587:17)
at require (internal/module.js:11:18)
at Object. (C:\Users\fimi\Desktop\setup\strategies\PortfolioManager.js:10:12)
at Module._compile (module.js:643:30)
at Object.Module._extensions..js (module.js:654:10)
at Module.load (module.js:556:32)
at tryModuleLoad (module.js:499:12)
at Function.Module._load (module.js:491:3)
xxx POST /api/backtest 500 163ms -

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment