From 964893e34086cda110ab9105eb037396c56a5240 Mon Sep 17 00:00:00 2001 From: Vision Agency Date: Fri, 26 Jan 2018 15:38:41 -0800 Subject: [PATCH 01/10] subtract candle length from start time to calculate isPremature --- plugins/tradingAdvisor/baseTradingMethod.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/tradingAdvisor/baseTradingMethod.js b/plugins/tradingAdvisor/baseTradingMethod.js index 75f0db939..7197b96dd 100644 --- a/plugins/tradingAdvisor/baseTradingMethod.js +++ b/plugins/tradingAdvisor/baseTradingMethod.js @@ -223,10 +223,17 @@ Base.prototype.propogateTick = function(candle) { // than minimally needed. In that case check // whether candle start time is > startTime var isPremature; - if(mode === 'realtime') - isPremature = candle.start < startTime; - else + + // Subtract number of minutes in current candle for instant start + let startTimeMinusCandleSize = startTime.clone(); + startTimeMinusCandleSize.subtract(this.tradingAdvisor.candleSize, "minutes"); + + if(mode === 'realtime'){ + isPremature = candle.start < startTimeMinusCandleSize; + } + else{ isPremature = false; + } if(isAllowedToCheck && !isPremature) { this.log(candle); From 0cac468952289f4d6a85c953777102c6083e5655 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Tue, 30 Jan 2018 00:00:53 -0800 Subject: [PATCH 02/10] Move startTimeMinusCandleSize inside realtime if check --- plugins/tradingAdvisor/baseTradingMethod.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/tradingAdvisor/baseTradingMethod.js b/plugins/tradingAdvisor/baseTradingMethod.js index 7197b96dd..b132a2e66 100644 --- a/plugins/tradingAdvisor/baseTradingMethod.js +++ b/plugins/tradingAdvisor/baseTradingMethod.js @@ -224,11 +224,11 @@ Base.prototype.propogateTick = function(candle) { // whether candle start time is > startTime var isPremature; - // Subtract number of minutes in current candle for instant start - let startTimeMinusCandleSize = startTime.clone(); - startTimeMinusCandleSize.subtract(this.tradingAdvisor.candleSize, "minutes"); - if(mode === 'realtime'){ + // Subtract number of minutes in current candle for instant start + let startTimeMinusCandleSize = startTime.clone(); + startTimeMinusCandleSize.subtract(this.tradingAdvisor.candleSize, "minutes"); + isPremature = candle.start < startTimeMinusCandleSize; } else{ From 1fe8aff4fb60d77a93ce9a506dcf198f511a5b08 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Tue, 6 Feb 2018 01:27:24 -0800 Subject: [PATCH 03/10] return the indicator after adding it --- plugins/tradingAdvisor/baseTradingMethod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tradingAdvisor/baseTradingMethod.js b/plugins/tradingAdvisor/baseTradingMethod.js index b132a2e66..59741c2b3 100644 --- a/plugins/tradingAdvisor/baseTradingMethod.js +++ b/plugins/tradingAdvisor/baseTradingMethod.js @@ -298,7 +298,7 @@ Base.prototype.addIndicator = function(name, type, parameters) { if(this.setup) util.die('Can only add indicators in the init method!'); - this.indicators[name] = new Indicators[type](parameters); + return this.indicators[name] = new Indicators[type](parameters); // some indicators need a price stream, others need full candles } From 55b3fe526ed92b5dd0299c685c9ebc7d4988b705 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Sat, 24 Feb 2018 04:42:22 -0800 Subject: [PATCH 04/10] develop trade class --- plugins/trader/portfolioManager.js | 249 +++----------------------- plugins/trader/trade.js | 277 +++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+), 220 deletions(-) create mode 100644 plugins/trader/trade.js diff --git a/plugins/trader/portfolioManager.js b/plugins/trader/portfolioManager.js index ef7ebf296..80d2ad614 100644 --- a/plugins/trader/portfolioManager.js +++ b/plugins/trader/portfolioManager.js @@ -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); @@ -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(); @@ -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 @@ -137,47 +139,11 @@ 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; @@ -185,147 +151,38 @@ Manager.prototype.getMinimum = function(price) { 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){ + // stop the current trade, and then re-run this method + this.currentTrade.cancelLastOrder(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}) }; - -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; @@ -338,55 +195,6 @@ 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) { @@ -394,4 +202,5 @@ Manager.prototype.logPortfolio = function() { }); }; + module.exports = Manager; diff --git a/plugins/trader/trade.js b/plugins/trader/trade.js new file mode 100644 index 000000000..f30250fd3 --- /dev/null +++ b/plugins/trader/trade.js @@ -0,0 +1,277 @@ +/* + @askmike + I propose to create something a bit more clear and subtle: a new class that is responsible for + doing a single trade (let's call him broker or execution strategy or so), it needs to: + + - Figure out if there is a limit on the size of the trade we can do. + - If we can't do any trade (low funds) don't do anything. + - If we can do a trade figure out what kind of trade we aim to do (sell X USD) and trigger an event (see #1850). + - Try to buy sell according to current "limit order do not cross spread" strategy (explained here). + - Only when fully completed figure out trade statistics (average execution rate, amount that was traded, etc). + - Only after this upstream a trade event with the statistics. + + The reason I want to pull this out is so that in the future we can have different execution strategies: + the current one tries to get the best price - at the cost of execution speed (since it will always + make orders it might take a long time before the trade is completed, especially when trying to catch + a trend). Once it's split out we can easily create another one that will do a market order instead. + Or one that tries to split the order out into multiple small ones (bigger traders need ways to take + pressure of the market for example). + +*/ + +/* + The Trade class is responsible for overseeing potentially multiple orders + to execute a trade that completely moves a position instantiated by the portfolio manager. +*/ + +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') + +class Trade{ + constructor(manager,settings){ + this.manager = manager + this.exchange = manager.exchange + this.currency = manager.currency + this.asset = manager.asset + this.action = settings.action + this.isActive = true + + this.orderIds = [] + + this.doTrade() + } + + // This function makes sure the limit order gets submitted + // to the exchange and initiates order registers watchers. + doTrade(retry) { + // if we are still busy executing the last trade + // cancel that one (and ignore results = assume not filled) + if(!retry && _.size(this.orderIds)) + return this.cancelLastOrder(() => this.doTrade()); + + var act = function() { + var amount, price; + + if(this.action === 'BUY') { + + amount = this.manager.getBalance(this.currency) / this.manager.ticker.ask; + if(amount > 0){ + price = this.manager.ticker.bid; + this.buy(amount, price); + } + } else if(this.action === 'SELL') { + + amount = this.manager.getBalance(this.asset) - this.manager.keepAsset; + if(amount > 0){ + price = this.manager.ticker.ask; + this.sell(amount, price); + } + } + }; + async.series([ + this.manager.setTicker, + this.manager.setPortfolio, + this.manager.setFee + ], _.bind(act, this)); + + }; + + // 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) + buy(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); + } + + if (_.has(this.exchange, 'getLotSize')) { + this.exchange.getLotSize('buy', amount, price, _.bind(process)); + } else { + minimum = this.manager.getMinimum(price); + process(undefined, { amount: amount, price: price }); + } + }; + + // 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) + sell(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 + ); + } + + 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.manager.getMinimum(price); + process(undefined, { amount: amount, price: price }); + } + }; + + + // check whether the order got fully filled + // if it is not: cancel & instantiate a new order + checkOrder() { + 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.orderIds), _.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.doTrade(this.action, true), + +moment.duration(wait, 'seconds') + ); + return; + } + + this.doTrade(this.action, true); + } + + this.exchange.checkOrder(_.last(this.orderIds), _.bind(handleCheckResult, this)); + } + + cancelLastOrder(done) { + this.exchange.cancelOrder(_.last(this.orderIds), alreadyFilled => { + if(alreadyFilled) + return this.relayOrder(done); + + this.orderIds = []; + done(); + }); + } + + noteOrder(err, order) { + if(err) { + util.die(err); + } + + this.orderIds.push(order); + + // If unfilled, cancel and replace order with adjusted price + let cancelDelay = this.manager.conf.orderUpdateDelay || 1; + setTimeout(this.checkOrder, util.minToMs(cancelDelay)); + }; + + relayOrder(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.orderIds = []; + + if(_.isFunction(done)) + done(); + }); + + } + + var getOrders = _.map( + this.orderIds, + order => next => this.exchange.getOrder(order, next) + ); + + async.series(getOrders, relay); + } + +} + +module.exports = Trade \ No newline at end of file From 02c1d4d3e10e15d8e2d2653b07abdad3f1012626 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Sat, 24 Feb 2018 06:26:28 -0800 Subject: [PATCH 05/10] make fixes after live market test --- package-lock.json | 36 +++++++++++++------------- plugins/trader/portfolioManager.js | 4 +-- plugins/trader/trade.js | 41 ++++++++++++++++++++++-------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 295d68901..69274c481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,15 @@ } } }, + "JSONStream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, "accepts": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", @@ -2081,15 +2090,6 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" }, - "JSONStream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -2794,9 +2794,9 @@ "resolved": "https://registry.npmjs.org/node.bittrex.api/-/node.bittrex.api-0.4.4.tgz", "integrity": "sha512-zNrwiSufttRBfPeSJfQLRDd9AHQuAL2IVxJEdEtNvwqvqHsdRvPkiQfANOzPy+0jFM/J8/t6/+gJ8Df+0GkgiQ==", "requires": { + "JSONStream": "1.3.1", "event-stream": "3.3.4", "jsonic": "0.3.0", - "JSONStream": "1.3.1", "request": "2.83.0", "signalr-client": "0.0.17" } @@ -4424,14 +4424,6 @@ } } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -4441,6 +4433,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "0.0.5", "bundled": true diff --git a/plugins/trader/portfolioManager.js b/plugins/trader/portfolioManager.js index 80d2ad614..809704adc 100644 --- a/plugins/trader/portfolioManager.js +++ b/plugins/trader/portfolioManager.js @@ -160,8 +160,8 @@ Manager.prototype.trade = function(what) { // if an active trade is currently happening if(this.currentTrade && this.currentTrade.isActive){ if(this.currentTrade.action !== what){ - // stop the current trade, and then re-run this method - this.currentTrade.cancelLastOrder(makeNewTrade) + // 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 } diff --git a/plugins/trader/trade.js b/plugins/trader/trade.js index f30250fd3..9716c6f77 100644 --- a/plugins/trader/trade.js +++ b/plugins/trader/trade.js @@ -44,12 +44,30 @@ class Trade{ this.orderIds = [] + log.debug("creating new Trade class to", this.action, this.asset + "/" + this.currency) + this.doTrade() } + stop(callback){ + + this.cancelLastOrder(()=>{ + + this.isActive = false + log.debug("stopping Trade class from", this.action + "ING", this.asset + "/" + this.currency) + if(_.isFunction(callback)) + callback() + + }) + + } + // This function makes sure the limit order gets submitted // to the exchange and initiates order registers watchers. doTrade(retry) { + if(!this.isActive) + return false + // if we are still busy executing the last trade // cancel that one (and ignore results = assume not filled) if(!retry && _.size(this.orderIds)) @@ -111,7 +129,7 @@ class Trade{ order.price ); - this.exchange.buy(order.amount, order.price, this.noteOrder); + this.exchange.buy(order.amount, order.price, _.bind(this.noteOrder,this) ); } if (_.has(this.exchange, 'getLotSize')) { @@ -151,7 +169,7 @@ class Trade{ order.price ); - this.exchange.sell(order.amount, order.price, this.noteOrder); + this.exchange.sell(order.amount, order.price, _.bind(this.noteOrder,this)); } if (_.has(this.exchange, 'getLotSize')) { @@ -174,7 +192,8 @@ class Trade{ return; } - log.info(this.action, 'was successfull'); + log.debug("Trade class was successful", this.action + "ING", this.asset + "/" + this.currency) + this.isActive = false; this.relayOrder(); } @@ -183,19 +202,19 @@ class Trade{ if(alreadyFilled) return; - if(this.exchangeMeta.forceReorderDelay) { + if(this.manager.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.doTrade(this.action, true), + () => this.doTrade(true), +moment.duration(wait, 'seconds') ); return; } - this.doTrade(this.action, true); + this.doTrade(true); } this.exchange.checkOrder(_.last(this.orderIds), _.bind(handleCheckResult, this)); @@ -220,7 +239,7 @@ class Trade{ // If unfilled, cancel and replace order with adjusted price let cancelDelay = this.manager.conf.orderUpdateDelay || 1; - setTimeout(this.checkOrder, util.minToMs(cancelDelay)); + setTimeout(_.bind(this.checkOrder,this), util.minToMs(cancelDelay)); }; relayOrder(done) { @@ -238,10 +257,10 @@ class Trade{ }); async.series([ - this.setPortfolio, - this.setTicker + this.manager.setPortfolio, + this.manager.setTicker ], () => { - const portfolio = this.convertPortfolio(this.portfolio); + const portfolio = this.manager.convertPortfolio(this.manager.portfolio); this.emit('trade', { date, @@ -274,4 +293,6 @@ class Trade{ } +util.makeEventEmitter(Trade) + module.exports = Trade \ No newline at end of file From 0568115ca3adf86cef4ba4a91d9e4b4649448a60 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Sat, 24 Feb 2018 06:29:39 -0800 Subject: [PATCH 06/10] line spacing --- plugins/trader/trade.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/trader/trade.js b/plugins/trader/trade.js index 9716c6f77..650c4ebfd 100644 --- a/plugins/trader/trade.js +++ b/plugins/trader/trade.js @@ -41,7 +41,6 @@ class Trade{ this.asset = manager.asset this.action = settings.action this.isActive = true - this.orderIds = [] log.debug("creating new Trade class to", this.action, this.asset + "/" + this.currency) @@ -50,16 +49,12 @@ class Trade{ } stop(callback){ - this.cancelLastOrder(()=>{ - this.isActive = false log.debug("stopping Trade class from", this.action + "ING", this.asset + "/" + this.currency) if(_.isFunction(callback)) callback() - }) - } // This function makes sure the limit order gets submitted From 6e8e8ed336718eb529eb486c6e1ee005d180b568 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Tue, 27 Feb 2018 04:01:16 -0800 Subject: [PATCH 07/10] rollback package-lock.json to develop branch --- package-lock.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69274c481..295d68901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,15 +94,6 @@ } } }, - "JSONStream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, "accepts": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", @@ -2090,6 +2081,15 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" }, + "JSONStream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -2794,9 +2794,9 @@ "resolved": "https://registry.npmjs.org/node.bittrex.api/-/node.bittrex.api-0.4.4.tgz", "integrity": "sha512-zNrwiSufttRBfPeSJfQLRDd9AHQuAL2IVxJEdEtNvwqvqHsdRvPkiQfANOzPy+0jFM/J8/t6/+gJ8Df+0GkgiQ==", "requires": { - "JSONStream": "1.3.1", "event-stream": "3.3.4", "jsonic": "0.3.0", + "JSONStream": "1.3.1", "request": "2.83.0", "signalr-client": "0.0.17" } @@ -4424,6 +4424,14 @@ } } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-width": { "version": "1.0.2", "bundled": true, @@ -4433,14 +4441,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "stringstream": { "version": "0.0.5", "bundled": true From 56a131cb25d5ea47abc947bdcbd3f72664871507 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Tue, 27 Feb 2018 04:05:02 -0800 Subject: [PATCH 08/10] link to discussion: --- plugins/trader/trade.js | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/plugins/trader/trade.js b/plugins/trader/trade.js index 650c4ebfd..5452dbd93 100644 --- a/plugins/trader/trade.js +++ b/plugins/trader/trade.js @@ -1,27 +1,8 @@ -/* - @askmike - I propose to create something a bit more clear and subtle: a new class that is responsible for - doing a single trade (let's call him broker or execution strategy or so), it needs to: - - - Figure out if there is a limit on the size of the trade we can do. - - If we can't do any trade (low funds) don't do anything. - - If we can do a trade figure out what kind of trade we aim to do (sell X USD) and trigger an event (see #1850). - - Try to buy sell according to current "limit order do not cross spread" strategy (explained here). - - Only when fully completed figure out trade statistics (average execution rate, amount that was traded, etc). - - Only after this upstream a trade event with the statistics. - - The reason I want to pull this out is so that in the future we can have different execution strategies: - the current one tries to get the best price - at the cost of execution speed (since it will always - make orders it might take a long time before the trade is completed, especially when trying to catch - a trend). Once it's split out we can easily create another one that will do a market order instead. - Or one that tries to split the order out into multiple small ones (bigger traders need ways to take - pressure of the market for example). - -*/ - /* The Trade class is responsible for overseeing potentially multiple orders to execute a trade that completely moves a position instantiated by the portfolio manager. + + Discussion about this class can be found at: https://github.com/askmike/gekko/issues/1942 */ var _ = require('lodash') From 10f38bef0c93c8cfddd23565c3cdfd776f741608 Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Mon, 19 Mar 2018 08:44:37 -0700 Subject: [PATCH 09/10] move methods from portfolio manager into trade class and new portfolio class --- plugins/trader/portfolio.js | 111 ++++++++++++++++++++ plugins/trader/portfolioManager.js | 163 ++++++----------------------- plugins/trader/trade.js | 131 ++++++++++++++++------- 3 files changed, 235 insertions(+), 170 deletions(-) create mode 100644 plugins/trader/portfolio.js diff --git a/plugins/trader/portfolio.js b/plugins/trader/portfolio.js new file mode 100644 index 000000000..6e0776446 --- /dev/null +++ b/plugins/trader/portfolio.js @@ -0,0 +1,111 @@ +/* + The Portfolio class holds the most recent data about the portfolio and ticker +*/ + +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') + +class Portfolio{ + constructor(conf,exchange){ + _.bindAll(this) + this.conf = conf + this.exchange = exchange + this.portfolio = {} + this.fee = null + this.ticker = null + } + + getBalance(fund) { + return this.getFund(fund).amount; + } + + // return the [fund] based on the data we have in memory + getFund(fund) { + return _.find(this.portfolio, function(f) { return f.name === fund}); + } + + // convert into the portfolio expected by the performanceAnalyzer + convertPortfolio(asset,currency) { // rename? + var asset = _.find(this.portfolio, a => a.name === this.conf.asset).amount; + var currency = _.find(this.portfolio, a => a.name === this.conf.currency).amount; + + return { + currency, + asset, + balance: currency + (asset * this.ticker.bid) + } + } + + logPortfolio() { + log.info(this.exchange.name, 'portfolio:'); + _.each(this.portfolio, function(fund) { + log.info('\t', fund.name + ':', parseFloat(fund.amount).toFixed(12)); + }); + }; + + setPortfolio(callback) { + let set = (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; + }); + + this.portfolio = portfolio; + + if(_.isEmpty(this.portfolio)) + this.emit('portfolioUpdate', this.convertPortfolio(this.conf.asset,this.conf.currency,this.ticker.bid)); + + if(_.isFunction(callback)) + callback(); + + } + + this.exchange.getPortfolio(set); + } + + setFee(callback) { + let set = (err, fee) => { + this.fee = fee; + + if(err) + util.die(err); + + if(_.isFunction(callback)) + callback(); + } + this.exchange.getFee(set); + } + + setTicker(callback) { + let set = (err, ticker) => { + this.ticker = ticker; + + if(err) + util.die(err); + + if(_.isFunction(callback)) + callback(); + } + this.exchange.getTicker(set); + } + +} + +util.makeEventEmitter(Portfolio) + +module.exports = Portfolio \ No newline at end of file diff --git a/plugins/trader/portfolioManager.js b/plugins/trader/portfolioManager.js index 809704adc..5b3008fde 100644 --- a/plugins/trader/portfolioManager.js +++ b/plugins/trader/portfolioManager.js @@ -1,12 +1,10 @@ /* - 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'); @@ -17,151 +15,64 @@ var log = require(dirs.core + 'log'); var async = require('async'); var checker = require(dirs.core + 'exchangeChecker.js'); var moment = require('moment'); +var Portfolio = require('./portfolio'); var Trade = require('./trade'); var Manager = function(conf) { - _.bindAll(this); + //_.bindAll(this); + this.conf = conf; 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); + let exchangeMeta = checker.settings(conf); + var Exchange = require(dirs.exchanges + exchangeMeta.slug); this.exchange = new Exchange(conf); - this.conf = conf; - this.portfolio = {}; - this.fee; - - 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; + // create a portfolio + this.portfolio = new Portfolio(conf,this.exchange); // 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; - } - }; // 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.debug('getting balance & fee from', this.exchange.name); + let prepare = () => { log.info('trading at', this.exchange.name, 'ACTIVE'); - log.info(this.exchange.name, 'trading fee will be:', this.fee * 100 + '%'); - this.logPortfolio(); - + log.info(this.exchange.name, 'trading fee will be:', this.portfolio.fee * 100 + '%'); // Move fee into Exchange class? + this.portfolio.logPortfolio(); callback(); - }; + } async.series([ - this.setTicker, - this.setPortfolio, - this.setFee - ], _.bind(prepare, this)); + this.portfolio.setFee.bind(this.portfolio), + this.portfolio.setTicker.bind(this.portfolio), + this.portfolio.setPortfolio.bind(this.portfolio) + ], prepare); + } -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; -}; - -Manager.prototype.getMinimum = function(price) { - if(this.minimalOrder.unit === 'currency') - return minimum = this.minimalOrder.amount / price; - else - return minimum = this.minimalOrder.amount; -}; - Manager.prototype.trade = function(what) { - var makeNewTrade = function(){ + let makeNewTrade = () => { + log.debug("PORTFOLIO MANAGER : trade() - makeNewTrade callback : creating a new Trade class to ", what, this.conf.asset, "/", this.conf.currency) this.newTrade(what) - }.bind(this) + } // 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) + this.currentTrade.deactivate(makeNewTrade) } else{ // do nothing, the trade is already going } @@ -175,32 +86,24 @@ Manager.prototype.trade = function(what) { // by invoking variable trade subclasses // - pass the trade a specific amount limited by a currency allowance Manager.prototype.newTrade = function(what) { + log.debug("PORTFOLIO MANAGER : newTrade() : creating a new Trade class to ", what, this.conf.asset, "/", this.conf.currency) + // push the current (asummed to be inactive) trade to the history if(this.currentTrade){ this.tradeHistory.push(this.currentTrade) - this.currentTrade = false - } - return this.currentTrade = new Trade(this,{action: what}) -}; - -// 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.logPortfolio = function() { - log.info(this.exchange.name, 'portfolio:'); - _.each(this.portfolio, function(fund) { - log.info('\t', fund.name + ':', parseFloat(fund.amount).toFixed(12)); - }); + return this.currentTrade = new Trade({ + action: what, + exchange:this.exchange, + currency: this.conf.currency, + asset: this.conf.asset, + portfolio: this.portfolio, + orderUpdateDelay: this.conf.orderUpdateDelay, + keepAsset: (this.conf.keepAsset) ? this.conf.keepAsset : false + }) }; + module.exports = Manager; diff --git a/plugins/trader/trade.js b/plugins/trader/trade.js index 5452dbd93..763bf35bc 100644 --- a/plugins/trader/trade.js +++ b/plugins/trader/trade.js @@ -15,33 +15,59 @@ var checker = require(dirs.core + 'exchangeChecker.js') var moment = require('moment') class Trade{ - constructor(manager,settings){ - this.manager = manager - this.exchange = manager.exchange - this.currency = manager.currency - this.asset = manager.asset - this.action = settings.action + constructor(conf){ + this.conf = conf + this.exchange = conf.exchange + this.portfolio = conf.portfolio + this.currency = conf.currency + this.asset = conf.asset + this.action = conf.action this.isActive = true + this.isDeactivating = false this.orderIds = [] - log.debug("creating new Trade class to", this.action, this.asset + "/" + this.currency) + this.exchangeMeta = checker.settings({exchange:this.exchange.name}); + + 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; + + log.debug("created new Trade class to", this.action, this.asset + "/" + this.currency) + + if(_.isNumber(conf.keepAsset)) { + log.debug('Keep asset is active. Will try to keep at least ' + conf.keepAsset + ' ' + conf.asset); + this.keepAsset = conf.keepAsset; + } else { + this.keepAsset = 0; + } this.doTrade() } - stop(callback){ - this.cancelLastOrder(()=>{ + deactivate(callback){ + this.isDeactivating = true + + log.debug("attempting to stop Trade class from", this.action + "ING", this.asset + "/" + this.currency) + + let done = () => { this.isActive = false - log.debug("stopping Trade class from", this.action + "ING", this.asset + "/" + this.currency) + log.debug("successfully stopped Trade class from", this.action + "ING", this.asset + "/" + this.currency) if(_.isFunction(callback)) callback() - }) + } + + if(_.size(this.orderIds)){ + this.cancelLastOrder(done) + } else { + done() + } } // This function makes sure the limit order gets submitted // to the exchange and initiates order registers watchers. doTrade(retry) { - if(!this.isActive) + if(!this.isActive || this.isDeactivating) return false // if we are still busy executing the last trade @@ -49,32 +75,33 @@ class Trade{ if(!retry && _.size(this.orderIds)) return this.cancelLastOrder(() => this.doTrade()); - var act = function() { + let act = () => { var amount, price; if(this.action === 'BUY') { - amount = this.manager.getBalance(this.currency) / this.manager.ticker.ask; + amount = this.portfolio.getBalance(this.currency) / this.portfolio.ticker.ask; if(amount > 0){ - price = this.manager.ticker.bid; + price = this.portfolio.ticker.bid; this.buy(amount, price); } } else if(this.action === 'SELL') { - amount = this.manager.getBalance(this.asset) - this.manager.keepAsset; + amount = this.portfolio.getBalance(this.asset) - this.keepAsset; if(amount > 0){ - price = this.manager.ticker.ask; + price = this.portfolio.ticker.ask; this.sell(amount, price); } } - }; + } + async.series([ - this.manager.setTicker, - this.manager.setPortfolio, - this.manager.setFee - ], _.bind(act, this)); + this.portfolio.setTicker.bind(this.portfolio), + this.portfolio.setPortfolio.bind(this.portfolio), + this.portfolio.setFee.bind(this.portfolio), + ], act); - }; + } // first do a quick check to see whether we can buy // the asset, if so BUY and keep track of the order @@ -82,6 +109,11 @@ class Trade{ buy(amount, price) { let minimum = 0; let process = (err, order) => { + + if(!this.isActive || this.isDeactivating){ + return log.debug(this.action, "trade class is no longer active") + } + // if order to small if(!order.amount || order.amount < minimum) { return log.warn( @@ -111,10 +143,10 @@ class Trade{ if (_.has(this.exchange, 'getLotSize')) { this.exchange.getLotSize('buy', amount, price, _.bind(process)); } else { - minimum = this.manager.getMinimum(price); + minimum = this.getMinimum(price); process(undefined, { amount: amount, price: price }); } - }; + } // first do a quick check to see whether we can sell // the asset, if so SELL and keep track of the order @@ -122,6 +154,11 @@ class Trade{ sell(amount, price) { let minimum = 0; let process = (err, order) => { + + if(!this.isActive || this.isDeactivating){ + return log.debug(this.action, "trade class is no longer active") + } + // if order to small if (!order.amount || order.amount < minimum) { return log.warn( @@ -151,24 +188,33 @@ class Trade{ if (_.has(this.exchange, 'getLotSize')) { this.exchange.getLotSize('sell', amount, price, _.bind(process)); } else { - minimum = this.manager.getMinimum(price); + minimum = this.getMinimum(price); process(undefined, { amount: amount, price: price }); } - }; + } // check whether the order got fully filled // if it is not: cancel & instantiate a new order checkOrder() { var handleCheckResult = function(err, filled) { + + if(this.isDeactivating){ + return log.debug("checkOrder() : ", this.action, "trade class is currently deactivating, stop check") + } + + if(!this.isActive){ + return log.debug("checkOrder() : ", this.action, "trade class is no longer active, stop check") + } + if(!filled) { log.info(this.action, 'order was not (fully) filled, cancelling and creating new order'); + log.debug("checkOrder() : cancelling last " + this.action + " order ID : ", _.last(this.orderIds)) this.exchange.cancelOrder(_.last(this.orderIds), _.bind(handleCancelResult, this)); - return; } - log.debug("Trade class was successful", this.action + "ING", this.asset + "/" + this.currency) + log.info("Trade class was successful", this.action + "ING", this.asset + "/" + this.currency) this.isActive = false; this.relayOrder(); @@ -178,7 +224,7 @@ class Trade{ if(alreadyFilled) return; - if(this.manager.exchangeMeta.forceReorderDelay) { + 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}!`); @@ -197,13 +243,13 @@ class Trade{ } cancelLastOrder(done) { + log.debug("checkOrder() : cancelling last " + this.action + " order ID : ", _.last(this.orderIds)) this.exchange.cancelOrder(_.last(this.orderIds), alreadyFilled => { if(alreadyFilled) return this.relayOrder(done); - - this.orderIds = []; done(); }); + } noteOrder(err, order) { @@ -214,13 +260,13 @@ class Trade{ this.orderIds.push(order); // If unfilled, cancel and replace order with adjusted price - let cancelDelay = this.manager.conf.orderUpdateDelay || 1; + let cancelDelay = this.conf.orderUpdateDelay || 1; setTimeout(_.bind(this.checkOrder,this), util.minToMs(cancelDelay)); - }; + } relayOrder(done) { // look up all executed orders and relay average. - var relay = (err, res) => { + let relay = (err, res) => { var price = 0; var amount = 0; @@ -233,10 +279,10 @@ class Trade{ }); async.series([ - this.manager.setPortfolio, - this.manager.setTicker + this.portfolio.setTicker.bind(this.portfolio), + this.portfolio.setPortfolio.bind(this.portfolio) ], () => { - const portfolio = this.manager.convertPortfolio(this.manager.portfolio); + const portfolio = this.portfolio.convertPortfolio(this.asset,this.currency); this.emit('trade', { date, @@ -251,8 +297,6 @@ class Trade{ action: this.action.toLowerCase() }); - this.orderIds = []; - if(_.isFunction(done)) done(); }); @@ -267,6 +311,13 @@ class Trade{ async.series(getOrders, relay); } + getMinimum(price) { + if(this.minimalOrder.unit === 'currency') + return minimum = this.minimalOrder.amount / price; + else + return minimum = this.minimalOrder.amount; + } + } util.makeEventEmitter(Trade) From 180e7451d3e3a798d3584d90bbae544812b7d2ed Mon Sep 17 00:00:00 2001 From: Anson Phong Date: Fri, 30 Mar 2018 19:29:55 -0700 Subject: [PATCH 10/10] cleanup logging, comments --- plugins/trader/portfolio.js | 2 +- plugins/trader/portfolioManager.js | 18 +++-------------- plugins/trader/trade.js | 31 ++++++++++++------------------ 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/plugins/trader/portfolio.js b/plugins/trader/portfolio.js index 6e0776446..b40b451cc 100644 --- a/plugins/trader/portfolio.js +++ b/plugins/trader/portfolio.js @@ -58,7 +58,7 @@ class Portfolio{ let item = _.find(fullPortfolio, {name}); if(!item) { - log.debug(`Unable to find "${name}" in portfolio provided by exchange, assuming 0.`); + log.debug(`unable to find "${name}" in portfolio provided by exchange, assuming 0.`); item = {name, amount: 0}; } diff --git a/plugins/trader/portfolioManager.js b/plugins/trader/portfolioManager.js index 5b3008fde..c356f9448 100644 --- a/plugins/trader/portfolioManager.js +++ b/plugins/trader/portfolioManager.js @@ -1,10 +1,6 @@ /* 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) + all decisions are turned into Trades. */ var _ = require('lodash'); @@ -19,7 +15,6 @@ var Portfolio = require('./portfolio'); var Trade = require('./trade'); var Manager = function(conf) { - //_.bindAll(this); this.conf = conf; var error = checker.cantTrade(conf); @@ -44,7 +39,7 @@ var Manager = function(conf) { util.makeEventEmitter(Manager); Manager.prototype.init = function(callback) { - log.debug('getting balance & fee from', this.exchange.name); + log.debug('portfolioManager : getting balance & fee from', this.exchange.name); let prepare = () => { log.info('trading at', this.exchange.name, 'ACTIVE'); @@ -58,13 +53,11 @@ Manager.prototype.init = function(callback) { this.portfolio.setTicker.bind(this.portfolio), this.portfolio.setPortfolio.bind(this.portfolio) ], prepare); - } Manager.prototype.trade = function(what) { let makeNewTrade = () => { - log.debug("PORTFOLIO MANAGER : trade() - makeNewTrade callback : creating a new Trade class to ", what, this.conf.asset, "/", this.conf.currency) this.newTrade(what) } @@ -82,11 +75,8 @@ Manager.prototype.trade = function(what) { }; // 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) { - log.debug("PORTFOLIO MANAGER : newTrade() : creating a new Trade class to ", what, this.conf.asset, "/", this.conf.currency) + log.debug("portfolioManager : newTrade() : creating a new Trade class to ", what, this.conf.asset, "/", this.conf.currency) // push the current (asummed to be inactive) trade to the history if(this.currentTrade){ @@ -104,6 +94,4 @@ Manager.prototype.newTrade = function(what) { }) }; - - module.exports = Manager; diff --git a/plugins/trader/trade.js b/plugins/trader/trade.js index 763bf35bc..baf59aaf8 100644 --- a/plugins/trader/trade.js +++ b/plugins/trader/trade.js @@ -1,7 +1,6 @@ /* The Trade class is responsible for overseeing potentially multiple orders - to execute a trade that completely moves a position instantiated by the portfolio manager. - + to execute a trade that completely moves a position. Discussion about this class can be found at: https://github.com/askmike/gekko/issues/1942 */ @@ -36,7 +35,7 @@ class Trade{ log.debug("created new Trade class to", this.action, this.asset + "/" + this.currency) if(_.isNumber(conf.keepAsset)) { - log.debug('Keep asset is active. Will try to keep at least ' + conf.keepAsset + ' ' + conf.asset); + log.debug('keep asset is active. will try to keep at least ' + conf.keepAsset + ' ' + conf.asset); this.keepAsset = conf.keepAsset; } else { this.keepAsset = 0; @@ -77,16 +76,13 @@ class Trade{ let act = () => { var amount, price; - if(this.action === 'BUY') { - amount = this.portfolio.getBalance(this.currency) / this.portfolio.ticker.ask; if(amount > 0){ price = this.portfolio.ticker.bid; this.buy(amount, price); } } else if(this.action === 'SELL') { - amount = this.portfolio.getBalance(this.asset) - this.keepAsset; if(amount > 0){ price = this.portfolio.ticker.ask; @@ -100,7 +96,6 @@ class Trade{ this.portfolio.setPortfolio.bind(this.portfolio), this.portfolio.setFee.bind(this.portfolio), ], act); - } // first do a quick check to see whether we can buy @@ -108,16 +103,15 @@ class Trade{ // (amount is in asset quantity) buy(amount, price) { let minimum = 0; - let process = (err, order) => { + let process = (err, order) => { if(!this.isActive || this.isDeactivating){ return log.debug(this.action, "trade class is no longer active") } - // if order to small if(!order.amount || order.amount < minimum) { return log.warn( - 'Wanted to buy', + 'wanted to buy', this.asset, 'but the amount is too small ', '(' + parseFloat(amount).toFixed(8) + ' @', @@ -128,7 +122,7 @@ class Trade{ } log.info( - 'Attempting to BUY', + 'attempting to BUY', order.amount, this.asset, 'at', @@ -162,7 +156,7 @@ class Trade{ // if order to small if (!order.amount || order.amount < minimum) { return log.warn( - 'Wanted to buy', + 'wanted to buy', this.currency, 'but the amount is too small ', '(' + parseFloat(amount).toFixed(8) + ' @', @@ -173,7 +167,7 @@ class Trade{ } log.info( - 'Attempting to SELL', + 'attempting to SELL', order.amount, this.asset, 'at', @@ -200,21 +194,21 @@ class Trade{ var handleCheckResult = function(err, filled) { if(this.isDeactivating){ - return log.debug("checkOrder() : ", this.action, "trade class is currently deactivating, stop check") + return log.debug("trade : checkOrder() : ", this.action, "trade class is currently deactivating, stop check order") } if(!this.isActive){ - return log.debug("checkOrder() : ", this.action, "trade class is no longer active, stop check") + return log.debug("trade : checkOrder() : ", this.action, "trade class is no longer active, stop check order") } if(!filled) { log.info(this.action, 'order was not (fully) filled, cancelling and creating new order'); - log.debug("checkOrder() : cancelling last " + this.action + " order ID : ", _.last(this.orderIds)) + log.debug("trade : checkOrder() : cancelling last " + this.action + " order ID : ", _.last(this.orderIds)) this.exchange.cancelOrder(_.last(this.orderIds), _.bind(handleCancelResult, this)); return; } - log.info("Trade class was successful", this.action + "ING", this.asset + "/" + this.currency) + log.info("trade was successful", this.action + "ING", this.asset + "/" + this.currency) this.isActive = false; this.relayOrder(); @@ -243,7 +237,7 @@ class Trade{ } cancelLastOrder(done) { - log.debug("checkOrder() : cancelling last " + this.action + " order ID : ", _.last(this.orderIds)) + log.debug("trade : cancelLastOrder() : cancelling last " + this.action + " order ID : ", _.last(this.orderIds)) this.exchange.cancelOrder(_.last(this.orderIds), alreadyFilled => { if(alreadyFilled) return this.relayOrder(done); @@ -317,7 +311,6 @@ class Trade{ else return minimum = this.minimalOrder.amount; } - } util.makeEventEmitter(Trade)