UNPKG

molestiasconsectetur

Version:

Multi Exchange Crypto Currency Trading bot, Data Analysis Library and Strategy Back testing Engine

445 lines (409 loc) 22.4 kB
const {State} = require("../lib/states/States"); const util = require("../lib/utility/util"); const {Log} = require("../lib/utility/Log"); const Mock = require("../service/MockService").Service; const fs = require("fs"); /** * Class BackTest * * This class is used by the BitFoxEngine to run Backtest against a Strategy * */ /** * @typedef {Object} requiredCredentials The Required Credentials object for Exchanges * @property {String} apiKey The apiKey ,(Common auth method across exchanges) * @property {String} secret the secret key, (Common auth method across exchanges) * @property {any} uid Exchange dependent see ccxt documentation or consult with your target exchange * @property {any} login Exchange dependent see ccxt documentation or consult with your target exchange * @property {any} password Exchange dependent see ccxt documentation or consult with your target exchange * @property {any} twofa Exchange dependent see ccxt documentation or consult with your target exchange * @property {any} privateKey Exchange dependent see ccxt documentation or consult with your target exchange * @property {any} walletAddress Exchange dependent see ccxt documentation or consult with your target exchange * @property {any} token Exchange dependent see ccxt documentation or consult with your target exchange */ /** * @typedef {Object} options HTTP configuration for API calls only tinker with this if you now what you are doing * @property {String} defaultType The target for trading activities spot|futures|options|margin * @property {Boolean} adjustForTimeDifference Time difference adjustments * @property {Number} recvwindow the receive windows for responses! */ /** * @typedef {Object} backTestConfiguration Engine configuration options * @property {Number} amount Engine property, the base currency amount in this execution context * @property {Number} profitPct Engine property,the target % for profit taking in this execution context * @property {Number} stopLossPct Engine property,the stop loss target % in this execution context * @property {Number} fee Engine property,the fee % an exchange charges for purchasing and selling assets this execution context (Not fully supported yet!!) * @property {Boolean} life Engine property,flag to determine if this execution context should make real trade orders * @property {Number} interval Engine property,the interval in seconds the engine is using to periodically fetch OHLCV Historical Data and run execution contexts (Strategies & Alerting) * * @property {Boolean} Public Exchange property, flag to make sure only public API calls are made and Private API calls are mocked * @property {String} exchangeName Exchange property, the name of the traget exchange to use * @property {String} symbol Exchange property, the name of your trading pair i.e. BTCUSDT ETHUSDT etc. * @property {String} timeframe Exchange property, the time frame to choose for Historical Data Fetching * (Exchange dependent and Exchange must support historical Data retrieval) * @property {requiredCredentials} requiredCredentials property, The Required credentials the Exchange is asking for. (Exchange dependent) * @property {options} options property, Http nd Exchange configuration * * @property {Boolean} backTest Backtest property, flag to indicate if this execution context should run a backtest * @property {Number} requiredCandles Backtest property, the number of Historical Data Candles to fetch for each iteration * @property {Number} pollRate Backtest property, number of time to pull data from exchange * * @property {String} sidePreference Strategy property, the trading preference lon|short/biDirectional * @property {any} strategyExtras Strategy property, strategy specific arguments for custom implementations * * * @property {String} type Alert & Notification property, the alerting mechanism or type to use (Email|Slack|Telegram) * @property {String} notificationToken Alert & Notification property, the Authentication token for Notification support * @property {String} telegramChatId Alert & Notification property, (Telegram specific optional parameter to sync chatId at strat upi) * @property {String} emailFrom Alert & Notification property,(Email alert the email address the email is sent from) * @property {String} emailTo Alert & Notification property, (Email alert the email address the email is sent to) * @property {any} alertExtras Alert & Notification property, Alert specific arguments for custom implementations */ class BackTest { /** * * @param strategy {Strategy} The Target Strategy To Backtest * @param args {backTestConfiguration} options and/or parameters that where supplied to the BitFoxEngine during instantiation * @returns {BackTest} A instance of type Backtest */ static getBackTester(strategy, args) { return new BackTest(strategy, args) } /** * * @param strategy {Strategy} * @param args {backTestConfiguration} object with Backtest and specific Parameters usually supplied through BitFoxEngine at instantiation see BitFox Class for more */ constructor(strategy, args) { this.strategy = strategy; this.args = args; this.mockService =Mock.getService(args) this.tradeHistory = []; this.tradeDirection = null; this.profitTarget = this.args.profitPct this.stopLossTarget = this.args.stopLossPct || 0; this.funds = null; this.barAvgCount = []; this.barCount = 0; this.maxBarCount = 0; this.minBarCount = 1000000; this.stopOrderCount = 0; this.tradeSuccessCount = 0; this.adjustForBalance = false; this.maxLongDrawDown = 0; this.maxShortDrawDown = 0; } /** * * @returns {boolean} Check to see if a Trade Template has an exitOrder */ hasExitOrder(){ return this.tradeHistory.length>1 ||this.tradeHistory[this.tradeHistory.length-1].exitOrder != null;} /** * * @param candles {Array} open, high, low, close and volume values * @returns {Promise<boolean>} method to start Back testing */ async backTest(candles) { let me = this; let buff = await this.adjustForDelay(candles); let indexCount = 0; while (buff.length > 0) { let currentCandles = buff.splice(0, 1) let result = await this.strategy.run(indexCount, true); await this.processResult(result, indexCount, currentCandles[0]); indexCount++; } if(this.tradeHistory.length<=0){ return false; } let copy = JSON.parse(JSON.stringify(this.tradeHistory)); let avgBuff = []; let avgBuff2 = []; if (this.hasExitOrder()) { copy.forEach((trade) => { if (trade.exitTimeStamp != null) { let approximatedQuoteProfit = Math.abs((trade.exitOrder.amount * trade.exitOrder.price) -(trade.entryOrder.amount * trade.entryOrder.price) ); let approximatedBaseProfit = Math.abs(trade.exitOrder.amount - trade.entryOrder.amount); avgBuff.push(Math.abs(approximatedQuoteProfit)); avgBuff2.push(Math.abs(approximatedBaseProfit)); Log.trade(`Entry Time: ${trade.entryTimestamp} Exit Time ${trade.exitTimeStamp}`); Log.log(`Entry Order Price: ${trade.entryOrder.price} Exit Order price @ ${trade.exitOrder.price}`); Log.log(`Trade Side: ${trade.entryOrder.side === 'sell' ? 'Short' : 'Long'} `); if(trade.stopTriggered){ Log.short(`Stop Triggered`); this.stopOrderCount = this.stopOrderCount+1; } else{ this.tradeSuccessCount = this.tradeSuccessCount+1; } Log.log(`Total Bar Count: ${trade.totalBars}`); Log.log(`Approximated Quote Profit: ${approximatedQuoteProfit.toFixed(9)}`); Log.log(`Approximated Base Profit: ${approximatedBaseProfit.toFixed(9)}`); Log.log(`Max Draw Down: ${trade.maxDrawDown}`); let unrealizedQuoteLoss = Math.abs((trade.maxDrawDown*trade.amount)-trade.funds) let unrealizedBaseLoss = unrealizedQuoteLoss / trade.maxDrawDown; let adjustUnrealizedLoss = (trade.entryOrder.side === 'sell') ? unrealizedBaseLoss : unrealizedQuoteLoss; Log.log(`Unrealized Losses Drawdown: ${adjustUnrealizedLoss.toFixed(9)}`); Log.log(`Amount: ${trade.amount}`); Log.log(`Funds: ${trade.funds}`); console.log(); } }) Log.yellow(`Average Quote Profit: ${util.average(avgBuff)}`); Log.yellow(`Average Base Profit: ${util.average(avgBuff2)}`); } let funds = this.tradeHistory[this.tradeHistory.length-1].funds; Log.yellow(`Total Trades : ${this.tradeHistory.length}`); this.tradeHistory.length >= 2 ? Log.yellow(`Starting Funds: ${this.tradeHistory[0].funds} Current Funds ${funds}`) : null; this.tradeHistory.length >= 2 ? Log.yellow(`Starting Amount: ${this.tradeHistory[0].amount} Current Amount ${this.tradeHistory[this.tradeHistory.length - 1].amount}`) : null; Log.yellow(`Maximum Bars Per Trade: ${this.maxBarCount}`); Log.yellow(`Minimum Bars Per Trade: ${this.minBarCount}`); let tradesLength = (this.tradeHistory[this.tradeHistory.length-1].exitOrder !== null) ? this.tradeHistory.length : this.tradeHistory.length-1; let successRate = (this.tradeSuccessCount / tradesLength) * 100; Log.yellow(`Number Stop Orders Triggered : ${this.stopOrderCount}`); Log.yellow(`Number of Successful Trades : ${this.tradeSuccessCount}`); Log.yellow(`Overall Success Rate : ${successRate} %`); this.tradeHistory[this.tradeHistory.length-1].exitOrder === null ? console.log("Ongoing Trade \n",JSON.stringify(this.tradeHistory[this.tradeHistory.length-1].entryOrder,null,2)) : null; } /** * * @param candles {Array} open, high, low, close and volume values * @returns {Promise<any[]>} This method is responsible to adjust candle and indicator data to adjust for differences indicator data length, * and Candle Data lengths. Indicator Data with a long moving average period will usually have less Data than the original * Candle Data Array so we leverage this method to adjust the Array lengths */ async adjustForDelay(candles) { let data = (await this.strategy.setup(candles)).getIndicator(); // get the difference in length of both arrays let diff = Math.abs(candles.length - data.length); // prepare candle buffer i.e. drop difference in candles return candles.splice(diff, (candles.length - 1)); } /** * * @param result {{state:any, timestamp:date, custom:any, context:String}} Result coming back from the Strategy * @param indexCount {Number} this is a count to keep track of Indicator and Candle Data indexes * @param currentCandles {Array} open,high,low, close and volume values * @returns {Promise<void>} Processes the Strategy Response by evaluating the returned State of the Strategy */ async processResult(result, indexCount, currentCandles) { switch (result.state) { case State.STATE_ENTER_LONG : { this.handleStateLong(currentCandles); if(!this.adjustForBalance) {this.adjustForBalance = false}; } break; case State.STATE_ENTER_SHORT: { this.handleStateShort(currentCandles); if(!this.adjustForBalance) {this.adjustForBalance = false}; } break; case State.STATE_TAKE_PROFIT: { this.strategy.setState(State.STATE_PENDING); } break; case State.STATE_STOP_LOSS_TRIGGERED: { this.strategy.setState(State.STATE_PENDING); } break; case State.STATE_AWAIT_TAKE_PROFIT: { this.barCount++; let currentOrder = this.tradeHistory[this.tradeHistory.length - 1].entryOrder; if (this.tradeDirection === 'long' ) { this.calculateLongDrawDown(currentOrder, currentCandles); this.handleStateAwaitLongResult(currentOrder, currentCandles); } else { this.calculateShortDrawDown(currentOrder, currentCandles); this.handleStateAwaitShortResult(currentOrder, currentCandles); } } break; } } /** * * @param currentOrder {any} see ccxt documentation for order structure * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method to handle Await Short result meaning the Backtest engine has determined a Short * position is open, and it is now waiting to identify if the short is in profit or a stop order should be placed */ handleStateAwaitShortResult(currentOrder, currentCandles) { let pT = this.strategy.calculateShortProfitTarget(currentOrder.price, this.profitTarget) let sT = (this.stopLossTarget>0) ? this.strategy.calculateShortStopTarget(currentOrder.price,this.stopLossTarget) : 0; let isinProfitRange = util.priceInShortProfitRange(currentCandles[3], pT) let isInStopLossRange = (sT > 0) ? util.priceInShortStopRange(currentCandles[2], sT) : false; if (isinProfitRange) { this.completeTrade(currentCandles); }if(isInStopLossRange){ this.applyStopLoss(currentCandles) } this.strategy.setState((isinProfitRange) ? State.STATE_TAKE_PROFIT : (isInStopLossRange) ? State.STATE_STOP_LOSS_TRIGGERED : State.STATE_AWAIT_TAKE_PROFIT) } /** * * @param currentOrder {any} see ccxt documentation for order structure * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method to assign max Draw Down price value it will be used in the final output */ calculateLongDrawDown(currentOrder, currentCandles) { if(currentOrder.price < currentCandles[4] && currentOrder.price > this.maxLongDrawDown){ this.maxLongDrawDown =currentCandles[4]; } } /** * * @param currentOrder {any} see ccxt documentation for order structure * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method to assign max Draw Down price value it will be used in the final output */ calculateShortDrawDown(currentOrder, currentCandles) { if(currentOrder.price > currentCandles[4] && currentOrder.price > this.maxShortDrawDown){ this.maxShortDrawDown = currentCandles[4]; } } /** * * @param currentOrder {any} see ccxt documentation for order structure * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method to handle Await Short result meaning the Backtest engine has determined a Long * position is open, and it is now waiting to identify if the long is in profit or a stop order should be placed */ handleStateAwaitLongResult(currentOrder, currentCandles) { let pT = this.strategy.calculateLongProfitTarget(currentOrder.price, this.profitTarget) let sT = (this.stopLossTarget>0) ? this.strategy.calculateLongStopTarget(currentOrder.price,this.stopLossTarget) : 0; let isinProfitRange = util.priceInLongProfitRange(currentCandles[2], pT); let isInStopLossRange = (sT > 0) ? util.priceInLongStopRange(currentCandles[3], sT) : null; if (isinProfitRange) { this.completeTrade(currentCandles); }if(isInStopLossRange){ this.applyStopLoss(currentCandles) } this.strategy.setState((isinProfitRange) ? State.STATE_TAKE_PROFIT : (isInStopLossRange) ? State.STATE_STOP_LOSS_TRIGGERED : State.STATE_AWAIT_TAKE_PROFIT) } /** * * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method to handle state Short meaning the Backtest engine has determined a short * position can be entered */ handleStateShort(currentCandles) { this.adjustEntryBalance(currentCandles); this.strategy.setState(State.STATE_AWAIT_TAKE_PROFIT); let sO = this.mockService.limitSellOrder(this.args.symbol, this.args.amount, currentCandles[4],); this.tradeHistory.push( this.mockService.getTradeTemplate(currentCandles, sO, this.profitTarget, this.funds, this.args.amount, 'short') ) this.tradeDirection = 'short' } /** * * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method to handle state Short meaning the Backtest engine has determined a long * position can be entered */ handleStateLong(currentCandles) { this.adjustEntryBalance(currentCandles); this.strategy.setState(State.STATE_AWAIT_TAKE_PROFIT); let bO = this.mockService.limitBuyOrder(this.args.symbol, this.args.amount, currentCandles[4]) this.tradeHistory.push( this.mockService.getTradeTemplate(currentCandles, bO, this.profitTarget, this.funds, this.args.amount, 'long') ) this.tradeDirection = 'long'; } /** * * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} Method adjust internal balance this is just to keep track of fictional funds and base amounts * the Backtest engine will use it later to output Trade and Funding Statistics */ adjustUnrealizedBalance(currentCandles) { let trade = (this.tradeHistory.length > 0) ? this.tradeHistory[this.tradeHistory.length - 1] : null; let {funds, amount} = this.strategy.determineUnrealizedBalance(trade, currentCandles, this.args.amount); this.args.amount = amount; this.funds = funds; } /** * * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} this is a method to recalculate entry amounts after trades have been exited , * the Backtest engine will use it later to output Trade and Funding Statistics */ adjustEntryBalance(currentCandles) { if(this.adjustForBalance){ this.args.amount = (this.funds / currentCandles[4]); } let {funds, amount} = this.strategy.determineEntryBalance(currentCandles, this.args.amount); this.args.amount = amount; this.funds = funds; } /** * * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} this method is to apply a fictional stop loss order for backtesting statistics */ applyStopLoss(currentCandles) { let currentTrade = this.tradeHistory[this.tradeHistory.length - 1]; currentTrade.stopTriggered = true; this.completeTrade(currentCandles) } /** * * @param currentCandles {Array} open, high, low, close and volume values * @returns {void} this method completes ongoing trades i.e. it creates a exit order. It places a sell order when the entry was a long trade * and buy order when the entry was a sell order */ completeTrade(currentCandles) { let currentTrade = this.tradeHistory[this.tradeHistory.length - 1]; currentTrade.totalBars = this.barCount; this.barAvgCount.push(this.barCount) if (this.tradeDirection === 'long') { currentTrade.maxDrawDown = this.maxLongDrawDown; this.maxLongDrawDown = 0; this.executeSellOrder(currentTrade, currentCandles); } else { currentTrade.maxDrawDown = this.maxShortDrawDown; this.maxShortDrawDown = 0; this.executeBuyOrder(currentCandles, currentTrade); } if(this.maxBarCount < this.barCount){ this.maxBarCount = this.barCount} if(this.minBarCount > this.barCount){ this.minBarCount = this.barCount} this.barCount = 0; } /** * * @param currentCandles {Array} open, high, low, close and volume values * @param currentTrade {any} A internal representation of an ongoing trade * @returns {void} this method executes a fictional buy order */ executeBuyOrder(currentCandles, currentTrade) { // add exit buy order let buyBackAmount = this.funds / currentCandles[3]; currentTrade.exitOrder = this.mockService.marketBuyOrder(this.args.symbol, buyBackAmount, currentCandles[3]); this.adjustUnrealizedBalance(currentCandles) currentTrade.funds = this.funds; currentTrade.amount = buyBackAmount; this.args.amount = buyBackAmount; currentTrade.exitTimeStamp = new Date(currentCandles[0]); } /** * * @param currentCandles {Array} open, high, low, close and volume values * @param currentTrade {any} A internal representation of an ongoing trade * @returns {void} this method executes a fictional buy order */ executeSellOrder(currentTrade, currentCandles) { // add exit sell order currentTrade.exitOrder = this.mockService.marketSellOrder(this.args.symbol, this.args.amount, currentCandles[2],); this.adjustUnrealizedBalance(currentCandles) let newAmount = this.funds / currentCandles[2]; currentTrade.funds = this.funds; currentTrade.amount = newAmount; this.args.amount = newAmount; currentTrade.exitTimeStamp = new Date(currentCandles[0]); } } /** * * @type {{BackTestEngine: BackTest}} */ module.exports = {BackTestEngine: BackTest}