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;