From 3a25feef8879a64801f08ed1ca7c39597e5dea23 Mon Sep 17 00:00:00 2001 From: Luis Fernando Molina Date: Wed, 20 Nov 2019 12:04:22 +0100 Subject: [PATCH] Trading Simulation consuming indicators with new scheme --- Commons.js | 7 +- IndicatorBot.js | 4 +- MultiPeriod.js | 20 +- MultiPeriodDaily.js | 14 +- MultiPeriodMarket.js | 8 +- Project.njsproj | 3 + TradingBot.js | 139 +- TradingSimulation.js | 2893 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 3004 insertions(+), 84 deletions(-) create mode 100644 TradingSimulation.js diff --git a/Commons.js b/Commons.js index 706409c71..d38d935eb 100644 --- a/Commons.js +++ b/Commons.js @@ -164,6 +164,7 @@ */ let jsonifiedArray = [] + let previous; for (let i = 0; i < dataFile.length; i++) { @@ -175,7 +176,9 @@ } } + record.previous = previous; jsonifiedArray.push(record); + previous = record; } return jsonifiedArray @@ -405,7 +408,7 @@ } } - function writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, outputPeriodLabel, callBackFunction) { + function writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, timePeriodLabel, callBackFunction) { try { @@ -420,7 +423,7 @@ } let filePathRoot = contextSummary.devTeam + "/" + contextSummary.bot + "." + contextSummary.botVersion.major + "." + contextSummary.botVersion.minor + "/" + global.CLONE_EXECUTOR.codeName + "." + global.CLONE_EXECUTOR.version + "/" + global.EXCHANGE_NAME + "/" + contextSummary.dataSetVersion; - let filePath = filePathRoot + "/Output/" + contextSummary.product + "/" + contextSummary.dataset + "/" + outputPeriodLabel + dateForPath; + let filePath = filePathRoot + "/Output/" + contextSummary.product + "/" + contextSummary.dataset + "/" + timePeriodLabel + dateForPath; filePath += '/' + fileName fileStorage.createTextFile(contextSummary.devTeam, filePath, fileContent + '\n', onFileCreated); diff --git a/IndicatorBot.js b/IndicatorBot.js index ae0ac799b..74a547e44 100644 --- a/IndicatorBot.js +++ b/IndicatorBot.js @@ -44,7 +44,7 @@ } } - function start(dataFiles, timePeriod, outputPeriodLabel, currentDay, interExecutionMemory, callBackFunction) { + function start(dataFiles, timePeriod, timePeriodLabel, currentDay, interExecutionMemory, callBackFunction) { try { @@ -133,7 +133,7 @@ contextSummary.dataSetVersion = "dataSet.V1" let fileContent = commons.generateFileContent(outputData, outputDatasetNode.referenceParent.parentNode.record, resultsWithIrregularPeriods, processingDailyFiles, callBackFunction) - commons.writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, outputPeriodLabel, callBackFunction) + commons.writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, timePeriodLabel, callBackFunction) } diff --git a/MultiPeriod.js b/MultiPeriod.js index e877b2286..00961e350 100644 --- a/MultiPeriod.js +++ b/MultiPeriod.js @@ -63,7 +63,7 @@ let USER_BOT_MODULE = require("./TradingBot") botInstance = USER_BOT_MODULE.newTradingBot(bot, logger, UTILITIES, FILE_STORAGE); - botInstance.initialize(callBackFunction); + botInstance.initialize(pAssistant, callBackFunction); } catch (err) { logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack); @@ -284,12 +284,12 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriodsMarketFiles -> periodsLoopBody -> Entering function."); } const timePeriod = global.marketFilesPeriods[n][0]; - const outputPeriodLabel = global.marketFilesPeriods[n][1]; + const timePeriodLabel = global.marketFilesPeriods[n][1]; let dependencyIndex = 0; dataFiles = new Map(); - if (bot.VALUES_TO_USE.timePeriod === outputPeriodLabel) { + if (bot.VALUES_TO_USE.timePeriod === timePeriodLabel) { currentTimePeriod = global.marketFilesPeriods[n][0]; currentOutputPeriodName = global.marketFilesPeriods[n][1]; } @@ -320,7 +320,7 @@ return } - let filePath = dependency.product + '/' + dependency.dataSet + "/" + outputPeriodLabel; + let filePath = dependency.product + '/' + dependency.dataSet + "/" + timePeriodLabel; dataset.getTextFile(filePath, fileName, onFileReceived); @@ -575,22 +575,22 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriodsDailyFiles -> periodsLoopBody -> Entering function."); } const timePeriod = global.dailyFilePeriods[n][0]; - const outputPeriodLabel = global.dailyFilePeriods[n][1]; + const timePeriodLabel = global.dailyFilePeriods[n][1]; if (processConfig.framework.validPeriods !== undefined) { let validPeriod = false; for (let i = 0; i < processConfig.framework.validPeriods.length; i++) { let period = processConfig.framework.validPeriods[i]; - if (period === outputPeriodLabel) { validPeriod = true } + if (period === timePeriodLabel) { validPeriod = true } } if (validPeriod === false) { - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriodsDailyFiles -> periodsLoopBody -> Discarding period for not being listed as a valid period. -> outputPeriodLabel = " + outputPeriodLabel); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriodsDailyFiles -> periodsLoopBody -> Discarding period for not being listed as a valid period. -> timePeriodLabel = " + timePeriodLabel); } periodsControlLoop(); return; } } - if (bot.VALUES_TO_USE.timePeriod === outputPeriodLabel) { + if (bot.VALUES_TO_USE.timePeriod === timePeriodLabel) { currentTimePeriod = global.dailyFilePeriods[n][0]; currentOutputPeriodName = global.dailyFilePeriods[n][1]; } @@ -634,7 +634,7 @@ let filePath if (dependency.dataSet === "Multi-Period-Daily") { - filePath = dependency.product + '/' + dependency.dataSet + "/" + outputPeriodLabel + "/" + dateForPath; + filePath = dependency.product + '/' + dependency.dataSet + "/" + timePeriodLabel + "/" + dateForPath; } else { filePath = dependency.product + '/' + dependency.dataSet + "/" + dateForPath; } @@ -710,7 +710,7 @@ let dateForPath = bot.multiPeriodProcessDatetime.getUTCFullYear() + '/' + utilities.pad(bot.multiPeriodProcessDatetime.getUTCMonth() + 1, 2) + '/' + utilities.pad(bot.multiPeriodProcessDatetime.getUTCDate(), 2); let filePath if (dependency.dataSet === "Multi-Period-Daily") { - filePath = dependency.product + '/' + dependency.dataSet + "/" + outputPeriodLabel + "/" + dateForPath; + filePath = dependency.product + '/' + dependency.dataSet + "/" + timePeriodLabel + "/" + dateForPath; } else { filePath = dependency.product + '/' + dependency.dataSet + "/" + dateForPath; } diff --git a/MultiPeriodDaily.js b/MultiPeriodDaily.js index f11198fa7..a19d0c52c 100644 --- a/MultiPeriodDaily.js +++ b/MultiPeriodDaily.js @@ -370,16 +370,16 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriods -> periodsLoopBody -> Entering function."); } const timePeriod = global.dailyFilePeriods[n][0]; - const outputPeriodLabel = global.dailyFilePeriods[n][1]; + const timePeriodLabel = global.dailyFilePeriods[n][1]; if (processConfig.framework.validPeriods !== undefined) { let validPeriod = false; for (let i = 0; i < processConfig.framework.validPeriods.length; i++) { let period = processConfig.framework.validPeriods[i]; - if (period === outputPeriodLabel) { validPeriod = true } + if (period === timePeriodLabel) { validPeriod = true } } if (validPeriod === false) { - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriods -> periodsLoopBody -> Discarding period for not being listed as a valid period. -> outputPeriodLabel = " + outputPeriodLabel); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriods -> periodsLoopBody -> Discarding period for not being listed as a valid period. -> timePeriodLabel = " + timePeriodLabel); } periodsControlLoop(); return; } @@ -418,7 +418,7 @@ let dateForPath = previousDay.getUTCFullYear() + '/' + utilities.pad(previousDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(previousDay.getUTCDate(), 2); let filePath if (dependency.dataSet === "Multi-Period-Daily") { - filePath = dependency.product + '/' + dependency.dataSet + "/" + outputPeriodLabel + "/" + dateForPath; + filePath = dependency.product + '/' + dependency.dataSet + "/" + timePeriodLabel + "/" + dateForPath; } else { filePath = dependency.product + '/' + dependency.dataSet + "/" + dateForPath; } @@ -488,7 +488,7 @@ let dateForPath = bot.multiPeriodDailyProcessDatetime.getUTCFullYear() + '/' + utilities.pad(bot.multiPeriodDailyProcessDatetime.getUTCMonth() + 1, 2) + '/' + utilities.pad(bot.multiPeriodDailyProcessDatetime.getUTCDate(), 2); let filePath if (dependency.dataSet === "Multi-Period-Daily") { - filePath = dependency.product + '/' + dependency.dataSet + "/" + outputPeriodLabel + "/" + dateForPath; + filePath = dependency.product + '/' + dependency.dataSet + "/" + timePeriodLabel + "/" + dateForPath; } else { filePath = dependency.product + '/' + dependency.dataSet + "/" + dateForPath; } @@ -586,12 +586,12 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriods -> periodsLoopBody -> callTheBot -> Entering function."); } const timePeriod = global.dailyFilePeriods[n][0]; - const outputPeriodLabel = global.dailyFilePeriods[n][1]; + const timePeriodLabel = global.dailyFilePeriods[n][1]; botInstance.start( dataFiles, timePeriod, - outputPeriodLabel, + timePeriodLabel, bot.multiPeriodDailyProcessDatetime, interExecutionMemoryArray[n], onBotFinished); diff --git a/MultiPeriodMarket.js b/MultiPeriodMarket.js index 37311cef4..4ee15f3e8 100644 --- a/MultiPeriodMarket.js +++ b/MultiPeriodMarket.js @@ -148,7 +148,7 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriods -> periodsLoopBody -> Entering function."); } const timePeriod = global.marketFilesPeriods[n][0]; - const outputPeriodLabel = global.marketFilesPeriods[n][1]; + const timePeriodLabel = global.marketFilesPeriods[n][1]; let dependencyIndex = 0; dataFiles = new Map; @@ -177,7 +177,7 @@ let filePath if (dependency.dataSet === "Multi-Period-Market") { - filePath = dependency.product + '/' + dependency.dataSet + "/" + outputPeriodLabel; + filePath = dependency.product + '/' + dependency.dataSet + "/" + timePeriodLabel; } else { filePath = dependency.product + '/' + dependency.dataSet + "/" + dateForPath; } @@ -253,12 +253,12 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processTimePeriods -> periodsLoopBody -> callTheBot -> Entering function."); } const timePeriod = global.marketFilesPeriods[n][0]; - const outputPeriodLabel = global.marketFilesPeriods[n][1]; + const timePeriodLabel = global.marketFilesPeriods[n][1]; botInstance.start( dataFiles, timePeriod, - outputPeriodLabel, + timePeriodLabel, undefined, undefined, onBotFinished); diff --git a/Project.njsproj b/Project.njsproj index 8d7e2a509..22992fbc9 100644 --- a/Project.njsproj +++ b/Project.njsproj @@ -55,6 +55,9 @@ Code + + Code + Code diff --git a/TradingBot.js b/TradingBot.js index a8e1d7c2c..cb4891579 100644 --- a/TradingBot.js +++ b/TradingBot.js @@ -18,6 +18,8 @@ const COMMONS = require('./Commons.js'); let commons = COMMONS.newCommons(bot, logger, UTILITIES, FILE_STORAGE); + let assistant; + return thisObject; function finalize() { @@ -27,7 +29,7 @@ commons = undefined } - function initialize(callBackFunction) { + function initialize(pAssistant, callBackFunction) { try { @@ -36,6 +38,7 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } + assistant = pAssistant; callBackFunction(global.DEFAULT_OK_RESPONSE); } catch (err) { @@ -44,7 +47,7 @@ } } - function start(multiPeriodDataFiles, timePeriod, outputPeriodLabel, currentDay, interExecutionMemory, callBackFunction) { + function start(multiPeriodDataFiles, timePeriod, timePeriodLabel, currentDay, interExecutionMemory, callBackFunction) { try { @@ -100,73 +103,91 @@ } } - /* During the third phase, we need to generate the data of the different products this process produces an output */ - for (let i = 0; i < outputDatasets.length; i++) { + const TRADING_SIMULATION = require('./TradingSimulation.js'); + let tradingSimulation = TRADING_SIMULATION.newTradingSimulation(bot, logger, UTILITIES); - let outputDatasetNode = outputDatasets[i] - let jsonData // Just build data as Json objects - let outputData // Data built as a result of applying user defined code and formulas at the Data Building Procedure - let singularVariableName // Name of the variable for this product - let recordDefinition // Record as defined by the user at the UI - let fileContent // Here we store the contents of the new data built just before writing it to a file. - let resultsWithIrregularPeriods // A product will have irregular periods when the User Code inserts new result records at will, in contrast with normal procedure where the platform insert one record per loop execution. + tradingSimulation.runSimulation( + chart, + dataDependencies, + timePeriod, + timePeriodLabel, + currentDay, + interExecutionMemory, + assistant, + onSimulationFinished, + callBackFunction) - /* - For each outputDatasetNode in our process output, we will build the information based on our input products. - */ - recordDefinition = outputDatasetNode.referenceParent.parentNode.record - singularVariableName = outputDatasetNode.referenceParent.parentNode.code.singularVariableName + function onSimulationFinished() { + console.log("Finished") - /* Check Irregular Periods */ - if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.code.code.indexOf('results.push') >= 0) { - resultsWithIrregularPeriods = true + /* During the third phase, we need to generate the data of the different products this process produces an output */ + for (let i = 0; i < outputDatasets.length; i++) { + + let outputDatasetNode = outputDatasets[i] + let jsonData // Just build data as Json objects + let outputData // Data built as a result of applying user defined code and formulas at the Data Building Procedure + let singularVariableName // Name of the variable for this product + let recordDefinition // Record as defined by the user at the UI + let fileContent // Here we store the contents of the new data built just before writing it to a file. + let resultsWithIrregularPeriods // A product will have irregular periods when the User Code inserts new result records at will, in contrast with normal procedure where the platform insert one record per loop execution. + + /* + For each outputDatasetNode in our process output, we will build the information based on our input products. + */ + + recordDefinition = outputDatasetNode.referenceParent.parentNode.record + singularVariableName = outputDatasetNode.referenceParent.parentNode.code.singularVariableName + + /* Check Irregular Periods */ + if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.code.code.indexOf('results.push') >= 0) { + resultsWithIrregularPeriods = true + } + + /* Build the data */ + jsonData = commons.dataBuildingProcedure(products, mainDependency, recordDefinition, outputDatasetNode.referenceParent.parentNode.dataBuilding, singularVariableName, timePeriod, resultsWithIrregularPeriods) + + /* Add the calculated properties */ + if (outputDatasetNode.referenceParent.parentNode.calculations !== undefined) { + outputData = commons.calculationsProcedure(jsonData, recordDefinition, outputDatasetNode.referenceParent.parentNode.calculations, singularVariableName, timePeriod) + } else { + outputData = jsonData + } + products[outputDatasetNode.referenceParent.parentNode.code.pluralVariableName] = outputData } - /* Build the data */ - jsonData = commons.dataBuildingProcedure(products, mainDependency, recordDefinition, outputDatasetNode.referenceParent.parentNode.dataBuilding, singularVariableName, timePeriod, resultsWithIrregularPeriods) + /*At the fourth and last phase, we will save the new information generated into files corresponding to each output outputDatasetNode.*/ + let totalFilesWritten = 0 + for (let i = 0; i < outputDatasets.length; i++) { + let outputDatasetNode = outputDatasets[i] + let outputData = products[outputDatasetNode.referenceParent.parentNode.code.pluralVariableName] + let resultsWithIrregularPeriods // A product will have irregular periods when the User Code inserts new result records at will, in contrast with normal procedure where the platform insert one record per loop execution. + let contextSummary = {} - /* Add the calculated properties */ - if (outputDatasetNode.referenceParent.parentNode.calculations !== undefined) { - outputData = commons.calculationsProcedure(jsonData, recordDefinition, outputDatasetNode.referenceParent.parentNode.calculations, singularVariableName, timePeriod) - } else { - outputData = jsonData + /* Check Irregular Periods */ + if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.code.code.indexOf('results.push') >= 0) { + resultsWithIrregularPeriods = true + } + + /* Simplifying the access to basic info */ + + contextSummary.dataset = outputDatasetNode.referenceParent.code.codeName + contextSummary.product = outputDatasetNode.referenceParent.parentNode.code.codeName + contextSummary.bot = outputDatasetNode.referenceParent.parentNode.parentNode.code.codeName + contextSummary.devTeam = outputDatasetNode.referenceParent.parentNode.parentNode.parentNode.code.codeName + + /* This stuff is still hardcoded and unresolved. */ + contextSummary.botVersion = { + "major": 1, + "minor": 0 + } + contextSummary.dataSetVersion = "dataSet.V1" + + let fileContent = commons.generateFileContent(outputData, outputDatasetNode.referenceParent.parentNode.record, resultsWithIrregularPeriods, processingDailyFiles, callBackFunction) + commons.writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, timePeriodLabel, callBackFunction) } - products[outputDatasetNode.referenceParent.parentNode.code.pluralVariableName] = outputData } - /*At the fourth and last phase, we will save the new information generated into files corresponding to each output outputDatasetNode.*/ - let totalFilesWritten = 0 - for (let i = 0; i < outputDatasets.length; i++) { - let outputDatasetNode = outputDatasets[i] - let outputData = products[outputDatasetNode.referenceParent.parentNode.code.pluralVariableName] - let resultsWithIrregularPeriods // A product will have irregular periods when the User Code inserts new result records at will, in contrast with normal procedure where the platform insert one record per loop execution. - let contextSummary = {} - - /* Check Irregular Periods */ - if (outputDatasetNode.referenceParent.parentNode.dataBuilding.loop.code.code.indexOf('results.push') >= 0) { - resultsWithIrregularPeriods = true - } - - /* Simplifying the access to basic info */ - - contextSummary.dataset = outputDatasetNode.referenceParent.code.codeName - contextSummary.product = outputDatasetNode.referenceParent.parentNode.code.codeName - contextSummary.bot = outputDatasetNode.referenceParent.parentNode.parentNode.code.codeName - contextSummary.devTeam = outputDatasetNode.referenceParent.parentNode.parentNode.parentNode.code.codeName - - /* This stuff is still hardcoded and unresolved. */ - contextSummary.botVersion = { - "major": 1, - "minor": 0 - } - contextSummary.dataSetVersion = "dataSet.V1" - - let fileContent = commons.generateFileContent(outputData, outputDatasetNode.referenceParent.parentNode.record, resultsWithIrregularPeriods, processingDailyFiles, callBackFunction) - commons.writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, outputPeriodLabel, callBackFunction) - } - - function anotherFileWritten() { totalFilesWritten++ if (totalFilesWritten === outputDatasets.length) { diff --git a/TradingSimulation.js b/TradingSimulation.js new file mode 100644 index 000000000..a018512a7 --- /dev/null +++ b/TradingSimulation.js @@ -0,0 +1,2893 @@ +exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILITIES) { + + const FULL_LOG = true; + const LOG_FILE_CONTENT = false; + + const MODULE_NAME = "Trading Simulation -> " + bot.SESSION.name; + const ONE_DAY_IN_MILISECONDS = 24 * 60 * 60 * 1000; + const GMT_SECONDS = ':00.000 GMT+0000'; + + let thisObject = { + finalize: finalize, + runSimulation: runSimulation + }; + + let utilities = UTILITIES.newCloudUtilities(bot, logger); + + let percentageBandwidthAt = {} + let bollingerBandsAt = {} + let bollingerChannelsAt = {} + let bollingerSubChannelsAt = {} + let candlesAt = {} + + + return thisObject; + + function finalize() { + percentageBandwidthAt = undefined + bollingerBandsAt = undefined + bollingerChannelsAt = undefined + bollingerSubChannelsAt = undefined + candlesAt = undefined + thisObject = undefined + } + + function runSimulation( + chart, + dataDependencies, + timePeriod, + timePeriodLabel, + currentDay, + interExecutionMemory, + assistant, + callback, + callBackFunction) { + + try { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> Entering function."); } + let processingDailyFiles + if (timePeriod > global.dailyFilePeriods[0][0]) { + processingDailyFiles = false + } else { + processingDailyFiles = true + } + + let recordsArray = []; + let conditionsArray = []; + let strategiesArray = []; + let tradesArray = []; + + let tradingSystem = bot.DEFINITION.tradingSystem; + + /* Initial Default Values */ + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> bot.VALUES_TO_USE.timeRange.initialDatetime = " + bot.VALUES_TO_USE.timeRange.initialDatetime); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> bot.VALUES_TO_USE.timeRange.finalDatetime = " + bot.VALUES_TO_USE.timeRange.finalDatetime); } + + let timerToCloseStage = 0 + + /* Stop Loss Management */ + + const MIN_STOP_LOSS_VALUE = 1 // We can not let the stop be zero to avoid division by 0 error or infinity numbers as a result. + const MAX_STOP_LOSS_VALUE = Number.MAX_SAFE_INTEGER + let stopLoss = 0; + + /* Take Profit Management */ + + const MIN_TAKE_PROFIT_VALUE = 1 // We can not let the buy order be zero to avoid division by 0 error or infinity numbers as a result. + const MAX_TAKE_PROFIT_VALUE = Number.MAX_SAFE_INTEGER + let takeProfit = 0; + + /* Simulation Records */ + + let hitRatio = 0; + let ROI = 0; + let days = 0; + let positionDays = 0; + let anualizedRateOfReturn = 0; + let type = '""'; + let marketRate = 0; + let takePositionNow = false + let closePositionNow = false + + /* In some cases we need to know if we are positioned at the last candle of the calendar day, for that we need these variables. */ + + let lastInstantOfTheDay = 0 + if (currentDay) { + lastInstantOfTheDay = currentDay.valueOf() + ONE_DAY_IN_MILISECONDS - 1; + } + + /* + The following counters need to survive multiple executions of the similator and keep themselves reliable. + This is challenging when the simulation is executed using Daily Files, since each execution means a new + day and these counters are meant to be kept along the whole market. + + To overcome this problem, we use the interExecutionMemory to record the values of the current execution + when finish. But there are a few details: + + 1. When the process is at the head of the market, it executes multple times at the same day. + 2. The same code serves execution from Market Files. + 3. In Daily Files we are receiving candles from the current day and previous day, so we need to take care of + not adding to the counters duplicate info when processing the day before candles. + + To overcome these challenges we record the values of the counters and variables on the interExecutionMemory only when + the day is complete and if we have a current Day. That menas that for Market Files we will never use + interExecutionMemory. + */ + + /*Needed for statistics */ + let previousBalanceAssetA = 0; + let previousBalanceAssetB = 0; + + /* Position Management */ + let tradePositionRate = 0; + let tradePositionSize = 0; + + /* Strategy and Phase Management */ + let currentStrategyIndex = -1; + let strategyStage = 'No Stage'; + + let stopLossPhase = -1; + let stopLossStage = 'No Stage'; + + let takeProfitPhase = -1; + let takeProfitStage = 'No Stage'; + + /* These 2 objects will allow us to create separate files for each one of them. */ + + let currentStrategy = { + begin: 0, + end: 0, + status: 0, + number: 0, + beginRate: 0, + endRate: 0 + } + + let currentTrade = { + begin: 0, + end: 0, + status: 0, + profit: 0, + exitType: 0, + beginRate: 0, + endRate: 0 + } + + let balanceAssetA = bot.VALUES_TO_USE.initialBalanceA; + let balanceAssetB = bot.VALUES_TO_USE.initialBalanceB; + + let lastTradeProfitLoss = 0; + let profit = 0; + let lastTradeROI = 0; + + let roundtrips = 0; + let fails = 0; + let hits = 0; + let periods = 0; + let positionPeriods = 0; + + /* Usefull counters for conditions and formulas */ + + let distanceToLast = { + triggerOn: 0, + triggerOff: 0, + takePosition: 0, + closePosition: 0 + } + + /* Message to the Simulation Executor */ + + let orderId = 0; + let messageId = 0; + + let yesterday = {}; + + /* Initialization */ + + yesterday.stopLoss = 0 + yesterday.takeProfit = 0 + + yesterday.previousBalanceAssetA = previousBalanceAssetA + yesterday.previousBalanceAssetB = previousBalanceAssetB + + yesterday.tradePositionRate = tradePositionRate + yesterday.tradePositionSize = tradePositionSize + + yesterday.currentStrategyIndex = currentStrategyIndex + yesterday.strategyStage = strategyStage + + yesterday.stopLossPhase = stopLossPhase + yesterday.stopLossStage = stopLossStage + + yesterday.takeProfitPhase = takeProfitPhase + yesterday.takeProfitStage = takeProfitStage + + yesterday.currentStrategy = { + begin: 0, + end: 0, + status: 0, + number: 0, + beginRate: 0, + endRate: 0 + } + + yesterday.currentTrade = { + begin: 0, + end: 0, + status: 0, + profit: 0, + exitType: 0, + beginRate: 0, + endRate: 0 + } + + yesterday.balanceAssetA = balanceAssetA; + yesterday.balanceAssetB = balanceAssetB; + + yesterday.lastTradeProfitLoss = 0; + yesterday.profit = 0; + yesterday.lastTradeROI = 0; + + yesterday.roundtrips = 0; + yesterday.fails = 0; + yesterday.hits = 0; + yesterday.periods = 0; + yesterday.positionPeriods = 0; + + yesterday.distanceToLast = { + triggerOn: 0, + triggerOff: 0, + takePosition: 0, + closePosition: 0 + } + + yesterday.orderId = 0; + yesterday.messageId = 0; + + yesterday.hitRatio = 0; + yesterday.ROI = 0; + yesterday.anualizedRateOfReturn = 0; + + if (interExecutionMemory.roundtrips === undefined) { // This just means that the inter execution memory was never used before. + + /* Initialize the data structure we will use inter execution. */ + + interExecutionMemory.stopLoss = 0 + interExecutionMemory.takeProfit = 0 + + interExecutionMemory.previousBalanceAssetA = previousBalanceAssetA + interExecutionMemory.previousBalanceAssetB = previousBalanceAssetB + + interExecutionMemory.tradePositionRate = tradePositionRate + interExecutionMemory.tradePositionSize = tradePositionSize + + interExecutionMemory.currentStrategyIndex = currentStrategyIndex + interExecutionMemory.strategyStage = strategyStage + + interExecutionMemory.stopLossPhase = stopLossPhase + interExecutionMemory.stopLossStage = stopLossStage + + interExecutionMemory.takeProfitPhase = takeProfitPhase + interExecutionMemory.takeProfitStage = takeProfitStage + + interExecutionMemory.currentStrategy = { + begin: 0, + end: 0, + status: 0, + number: 0, + beginRate: 0, + endRate: 0 + } + + interExecutionMemory.currentTrade = { + begin: 0, + end: 0, + status: 0, + profit: 0, + exitType: 0, + beginRate: 0, + endRate: 0 + } + + interExecutionMemory.balanceAssetA = balanceAssetA; + interExecutionMemory.balanceAssetB = balanceAssetB; + + interExecutionMemory.lastTradeProfitLoss = lastTradeProfitLoss; + interExecutionMemory.profit = profit; + interExecutionMemory.lastTradeROI = lastTradeROI; + + interExecutionMemory.roundtrips = 0; + interExecutionMemory.fails = 0; + interExecutionMemory.hits = 0; + interExecutionMemory.periods = 0; + interExecutionMemory.positionPeriods = 0; + + interExecutionMemory.distanceToLast = { + triggerOn: 0, + triggerOff: 0, + takePosition: 0, + closePosition: 0 + } + + interExecutionMemory.orderId = 0; + interExecutionMemory.messageId = 0; + + interExecutionMemory.hitRatio = 0; + interExecutionMemory.ROI = 0; + interExecutionMemory.anualizedRateOfReturn = 0; + + interExecutionMemory.announcements = [] + + } else { + + /* We get the initial values from the day previous to the candles we receive at the current execution */ + + stopLoss = interExecutionMemory.stopLoss + takeProfit = interExecutionMemory.takeProfit + + previousBalanceAssetA = interExecutionMemory.previousBalanceAssetA + previousBalanceAssetB = interExecutionMemory.previousBalanceAssetB + + tradePositionRate = interExecutionMemory.tradePositionRate + tradePositionSize = interExecutionMemory.tradePositionSize + + currentStrategyIndex = interExecutionMemory.currentStrategyIndex + strategyStage = interExecutionMemory.strategyStage + + stopLossPhase = interExecutionMemory.stopLossPhase + stopLossStage = interExecutionMemory.stopLossStage + + takeProfitPhase = interExecutionMemory.takeProfitPhase + takeProfitStage = interExecutionMemory.takeProfitStage + + currentStrategy = { + begin: interExecutionMemory.currentStrategy.begin, + end: interExecutionMemory.currentStrategy.end, + status: interExecutionMemory.currentStrategy.status, + number: interExecutionMemory.currentStrategy.number, + beginRate: interExecutionMemory.currentStrategy.beginRate, + endRate: interExecutionMemory.currentStrategy.endRate + } + + currentTrade = { + begin: interExecutionMemory.currentTrade.begin, + end: interExecutionMemory.currentTrade.end, + status: interExecutionMemory.currentTrade.status, + profit: interExecutionMemory.currentTrade.profit, + exitType: interExecutionMemory.currentTrade.exitType, + beginRate: interExecutionMemory.currentTrade.beginRate, + endRate: interExecutionMemory.currentTrade.endRate + } + + if (currentDay) { + if (currentDay.valueOf() >= bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf() + ONE_DAY_IN_MILISECONDS) { // Only after the first day we start grabbing the balance from this memory. + + balanceAssetA = interExecutionMemory.balanceAssetA; + balanceAssetB = interExecutionMemory.balanceAssetB; + + yesterday.balanceAssetA = balanceAssetA; + yesterday.balanceAssetB = balanceAssetB; + + } + } + + lastTradeProfitLoss = interExecutionMemory.lastTradeProfitLoss; + profit = interExecutionMemory.profit; + lastTradeROI = interExecutionMemory.lastTradeROI; + + roundtrips = interExecutionMemory.roundtrips; + fails = interExecutionMemory.fails; + hits = interExecutionMemory.hits; + periods = interExecutionMemory.periods; + positionPeriods = interExecutionMemory.positionPeriods; + + distanceToLast.triggerOn = interExecutionMemory.distanceToLast.triggerOn + distanceToLast.triggerOff = interExecutionMemory.distanceToLast.triggerOff + distanceToLast.takePosition = interExecutionMemory.distanceToLast.takePosition + distanceToLast.closePosition = interExecutionMemory.distanceToLast.closePosition + + orderId = interExecutionMemory.orderId; // to be deprecated + messageId = interExecutionMemory.messageId; // to be deprecated + + hitRatio = interExecutionMemory.hitRatio; + ROI = interExecutionMemory.ROI; + anualizedRateOfReturn = interExecutionMemory.anualizedRateOfReturn; + + /* For the case that any of these variables are not updated during the main loop, we need to store their value at the yesterday structure, otherwise it would be lost. */ + + yesterday.stopLoss = stopLoss + yesterday.takeProfit = takeProfit + + yesterday.previousBalanceAssetA = previousBalanceAssetA + yesterday.previousBalanceAssetB = previousBalanceAssetB + + yesterday.tradePositionRate = tradePositionRate + yesterday.tradePositionSize = tradePositionSize + + yesterday.currentStrategyIndex = currentStrategyIndex; + yesterday.strategyStage = strategyStage; + + yesterday.stopLossPhase = stopLossPhase; + yesterday.stopLossStage = stopLossStage; + + yesterday.takeProfitPhase = takeProfitPhase; + yesterday.takeProfitStage = takeProfitStage; + + yesterday.currentStrategy = { + begin: currentStrategy.begin, + end: currentStrategy.end, + status: currentStrategy.status, + number: currentStrategy.number, + beginRate: currentStrategy.beginRate, + endRate: currentStrategy.endRate + } + + yesterday.currentTrade = { + begin: currentTrade.begin, + end: currentTrade.end, + status: currentTrade.status, + profit: currentTrade.profit, + exitType: currentTrade.exitType, + beginRate: currentTrade.beginRate, + endRate: currentTrade.endRate + } + + yesterday.lastTradeProfitLoss = lastTradeProfitLoss; + yesterday.profit = profit; + yesterday.lastTradeROI = lastTradeROI; + + yesterday.roundtrips = roundtrips; + yesterday.fails = fails; + yesterday.hits = hits; + yesterday.periods = periods; + yesterday.positionPeriods = positionPeriods; + + yesterday.distanceToLast.triggerOn = distanceToLast.triggerOn + yesterday.distanceToLast.triggerOff = distanceToLast.triggerOff + yesterday.distanceToLast.takePosition = distanceToLast.takePosition + yesterday.distanceToLast.closePosition = distanceToLast.closePosition + + yesterday.hitRatio = hitRatio; + yesterday.ROI = ROI; + yesterday.anualizedRateOfReturn = anualizedRateOfReturn; + } + + /* Main Array and Maps */ + + let propertyName = 'at' + timePeriodLabel.replace('-', ''); + let candles = chart[propertyName].candles + let currentChart = chart[propertyName] + + /* Last Candle */ + let lastCandle = candles[candles.length - 1]; + + /* Main Simulation Loop: We go thourgh all the candles at this time period. */ + let i + + /* For Loop Level heartbeat */ + let loopingDay + let previousLoopingDay + + initializeLoop() + + function initializeLoop() { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> initializeLoop -> Entering function."); } + + /* Estimate Initial Candle */ + + let firstEnd = candles[0].end + let targetEnd = bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf() + let diff = targetEnd - firstEnd + let amount = diff / timePeriod + + i = Math.trunc(amount) + if (i < 0) { i = 0 } + if (i > candles.length - 1) { + /* This will happen when the bot.VALUES_TO_USE.timeRange.initialDatetime is beyond the last candle available, meaning that the dataSet needs to be updated with more up-to-date data. */ + i = candles.length - 1 + } + + loop() + } + + function loop() { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Entering function."); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Processing candle # " + i); } + + let candle = candles[i]; + + /* Not processing while out of user-defined time range */ + + if (candle.end < bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Skipping Candle before the bot.VALUES_TO_USE.timeRange.initialDatetime."); } + controlLoop(); + return + } + if (candle.begin > bot.VALUES_TO_USE.timeRange.finalDatetime.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Skipping Candle after the bot.VALUES_TO_USE.timeRange.finalDatetime."); } + afterLoop(); + return + } + + /* Here we update at the chart data structure the objects for each product representing where we are currently standing at the simulation loop */ + if (processingDailyFiles) { + for (let j = 0; j < global.dailyFilePeriods.length; j++) { + + let mapKey = dailyFilePeriods[j][1] + let propertyName = 'at' + mapKey.replace('-', ''); + let thisChart = chart[propertyName] + + for (let k = 0; k < dataDependencies.length; k++) { + let dataDependencyNode = dataDependencies[k] + let singularVariableName = dataDependencyNode.referenceParent.parentNode.code.singularVariableName + let pluralVariableName = dataDependencyNode.referenceParent.parentNode.code.pluralVariableName + let currentElement = getElement(thisChart[pluralVariableName], candle, 'Daily' + '-' + mapKey + '-' + pluralVariableName) + thisChart[singularVariableName] = currentElement + } + } + } + + for (let j = 0; j < global.marketFilesPeriods.length; j++) { + + let mapKey = marketFilesPeriods[j][1] + let propertyName = 'at' + mapKey.replace('-', ''); + let thisChart = chart[propertyName] + + for (let k = 0; k < dataDependencies.length; k++) { + let dataDependencyNode = dataDependencies[k] + let singularVariableName = dataDependencyNode.referenceParent.parentNode.code.singularVariableName + let pluralVariableName = dataDependencyNode.referenceParent.parentNode.code.pluralVariableName + let currentElement = getElement(thisChart[pluralVariableName], candle, 'Market' + '-' + mapKey + '-' + pluralVariableName) + thisChart[singularVariableName] = currentElement + } + } + + /* While we are processing the previous day. */ + let positionedAtYesterday = false + if (currentDay) { + positionedAtYesterday = (candle.end < currentDay.valueOf()) + } + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Candle Begin @ " + (new Date(candle.begin)).toLocaleString()) } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Candle End @ " + (new Date(candle.end)).toLocaleString()) } + + /* Assistant Info */ + + let ticker = { + bid: candle.close, + ask: candle.close, + last: candle.close + } + + /* We will produce a simulation level heartbeat in order to inform the user this is running. */ + + loopingDay = new Date(Math.trunc(candle.begin / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS) + if (loopingDay.valueOf() !== previousLoopingDay) { + + let processingDate = loopingDay.getUTCFullYear() + '-' + utilities.pad(loopingDay.getUTCMonth() + 1, 2) + '-' + utilities.pad(loopingDay.getUTCDate(), 2); + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Simulation " + bot.sessionKey + " Loop # " + i + " @ " + processingDate) } + console.log("Jason -> " + MODULE_NAME + " -> runSimulation -> loop -> Simulation " + bot.sessionKey + " Loop # " + i + " @ " + processingDate) + + bot.sessionHeartBeat(processingDate) // tell the world we are alive and doing well + } + previousLoopingDay = loopingDay.valueOf() + + /* If any of the needed data dependencies is missing for this particular candle, then we jump the candle*/ + for (let k = 0; k < dataDependencies.length; k++) { + let dataDependencyNode = dataDependencies[k] + let singularVariableName = dataDependencyNode.referenceParent.parentNode.code.singularVariableName + let pluralVariableName = dataDependencyNode.referenceParent.parentNode.code.pluralVariableName + let product = currentChart[pluralVariableName] + let currentElement = currentChart[singularVariableName] + + if (currentElement === undefined) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Skipping Candle because " + singularVariableName + " is undefined."); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> For some indicators it is normal to start after a few candles."); } + controlLoop(); + return + } + } + + periods++; + days = periods * timePeriod / ONE_DAY_IN_MILISECONDS; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.periods = periods + } + + /* We skip the candle at the head of the market because i has not closed yet. */ + let candlesPerDay = ONE_DAY_IN_MILISECONDS / timePeriod + if (i === candles.length - 1) { + if ((candles.length < candlesPerDay) || (candles.length > candlesPerDay && candles.length < candlesPerDay * 2)) { + /*We are at the head of the market, thus we skip the last candle because it has not close yet. */ + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Skipping Candle because it is the last one and has not been closed yet."); } + controlLoop(); + return + /* Note here that in the last candle of the first day or the second day it will use an incomplete candle and partially calculated indicators. + if we skip these two periods, then there will be a hole in the file since the last period will be missing. */ + } + } + + } else { // We are processing Market Files + if (i === candles.length - 1) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Skipping Candle because it is the last one and has not been closed yet."); } + controlLoop(); + return + } + } + + let conditions = new Map; // Here we store the conditions values that will be use in the simulator for decision making. + let formulas = new Map; + let conditionsArrayRecord = []; // These are the records that will be saved in a file for the plotter to consume. + let conditionsArrayValues = []; // Here we store the conditions values that will be written on file for the plotter. + let formulasErrors = []; // Here we store the errors produced by all phase formulas. + let formulasValues = []; // Here we store the values produced by all phase formulas. + + /* We define and evaluate all conditions to be used later during the simulation loop. */ + + conditionsArrayRecord.push(candle.begin); + conditionsArrayRecord.push(candle.end); + + for (let j = 0; j < tradingSystem.strategies.length; j++) { + + let strategy = tradingSystem.strategies[j]; + + let positionSize = 0 + let positionRate = 0 + + /* Continue with the rest of the formulas and conditions */ + + let triggerStage = strategy.triggerStage + + if (triggerStage !== undefined) { + + if (triggerStage.triggerOn !== undefined) { + + for (let k = 0; k < triggerStage.triggerOn.situations.length; k++) { + + let situation = triggerStage.triggerOn.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'triggerStage' + '-' + 'triggerOn' + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + + if (triggerStage.triggerOff !== undefined) { + + for (let k = 0; k < triggerStage.triggerOff.situations.length; k++) { + + let situation = triggerStage.triggerOff.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'triggerStage' + '-' + 'triggerOff' + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + + if (triggerStage.takePosition !== undefined) { + + for (let k = 0; k < triggerStage.takePosition.situations.length; k++) { + + let situation = triggerStage.takePosition.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'triggerStage' + '-' + 'takePosition' + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + } + + let openStage = strategy.openStage + + if (openStage !== undefined) { + + /* Default Values*/ + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + positionSize = balanceAssetA; + positionRate = candle.close; + } else { + positionSize = balanceAssetB; + positionRate = candle.close; + } + + let initialDefinition = openStage.initialDefinition + + if (initialDefinition !== undefined) { + + if (tradePositionSize !== 0) { + positionSize = tradePositionSize + } else { + if (initialDefinition.positionSize !== undefined) { + if (initialDefinition.positionSize.formula !== undefined) { + try { + positionSize = eval(initialDefinition.positionSize.formula.code); + } catch (err) { + initialDefinition.positionSize.formula.error = err.message + } + if (isNaN(positionSize)) { + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + positionSize = balanceAssetA; + } else { + positionSize = balanceAssetB; + } + } else { + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + if (positionSize > balanceAssetA) { positionSize = balanceAssetA } + } else { + if (positionSize > balanceAssetB) { positionSize = balanceAssetB } + } + } + } + } + } + + if (tradePositionRate !== 0) { + positionRate = tradePositionRate + } else { + if (initialDefinition.positionRate !== undefined) { + if (initialDefinition.positionRate.formula !== undefined) { + try { + positionRate = eval(initialDefinition.positionRate.formula.code); + } catch (err) { + initialDefinition.positionRate.formula.error = err.message + } + if (isNaN(positionRate)) { + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + positionRate = candle.close; + } else { + positionRate = candle.close; + } + } + } + } + } + + if (initialDefinition.stopLoss !== undefined) { + + for (let p = 0; p < initialDefinition.stopLoss.phases.length; p++) { + + let phase = initialDefinition.stopLoss.phases[p]; + + /* Evaluate Formula */ + let formulaValue + let formulaError = '' + + if (phase.formula !== undefined) { + try { + formulaValue = eval(phase.formula.code); + if (formulaValue === Infinity) { + formulaError = "Formula evaluates to Infinity." + formulaValue = MAX_STOP_LOSS_VALUE + if (stopLossStage === 'Open Stage') { + formulaError = "WARNING: Formula evaluates to Infinity." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> initialDefinition.stopLoss -> MAX_STOP_LOSS_VALUE -> formulaError = " + formulaError); } + } + } + } catch (err) { + if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) { + /* + We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined. + */ + } else { + formulaError = err.message + } + } + if (isNaN(formulaValue)) { formulaValue = 0; } + if (formulaValue < MIN_STOP_LOSS_VALUE) { + formulaValue = MIN_STOP_LOSS_VALUE + if (stopLossStage === 'Open Stage') { + formulaError = "WARNING: Formula is evaluating below the MIN_STOP_LOSS_VALUE." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> initialDefinition.stopLoss -> MIN_STOP_LOSS_VALUE -> formulaError = " + formulaError); } + } + } + + formulasErrors.push('"' + formulaError + '"') + formulasValues.push(formulaValue) + let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + p; + formulas.set(key, formulaValue) + } + + /* next phase event */ + let nextPhaseEvent = phase.nextPhaseEvent; + if (nextPhaseEvent !== undefined) { + + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + + let situation = nextPhaseEvent.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + p + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + } + } + + if (initialDefinition.takeProfit !== undefined) { + + for (let p = 0; p < initialDefinition.takeProfit.phases.length; p++) { + + let phase = initialDefinition.takeProfit.phases[p]; + + /* Evaluate Formula */ + let formulaValue + let formulaError = '' + + if (phase.formula !== undefined) { + try { + formulaValue = eval(phase.formula.code); + if (formulaValue === Infinity) { + formulaValue = MAX_TAKE_PROFIT_VALUE + if (takeProfitStage === 'Open Stage') { + formulaError = "WARNING: Formula evaluates to Infinity." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> initialDefinition.takeProfit -> MAX_TAKE_PROFIT_VALUE -> formulaError = " + formulaError); } + } + } + } catch (err) { + if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) { + /* + We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined. + */ + } else { + formulaError = err.message + } + } + if (isNaN(formulaValue)) { formulaValue = 0; } + if (formulaValue < MIN_TAKE_PROFIT_VALUE) { + formulaValue = MIN_TAKE_PROFIT_VALUE + if (takeProfitStage === 'Open Stage') { + formulaError = "WARNING: Formula is evaluating below the MIN_TAKE_PROFIT_VALUE." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> initialDefinition.takeProfit -> MIN_TAKE_PROFIT_VALUE -> formulaError = " + formulaError); } + } + } + + formulasErrors.push('"' + formulaError + '"') + formulasValues.push(formulaValue) + let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + p; + formulas.set(key, formulaValue) + } + + /* next phase event */ + let nextPhaseEvent = phase.nextPhaseEvent; + if (nextPhaseEvent !== undefined) { + + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + + let situation = nextPhaseEvent.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + p + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + } + } + } + + strategy.positionSize = positionSize + strategy.positionRate = positionRate + } + + let manageStage = strategy.manageStage + + if (manageStage !== undefined) { + + if (manageStage.stopLoss !== undefined) { + + for (let p = 0; p < manageStage.stopLoss.phases.length; p++) { + + let phase = manageStage.stopLoss.phases[p]; + + /* Evaluate Formula */ + let formulaValue + let formulaError = '' + + if (phase.formula !== undefined) { + try { + formulaValue = eval(phase.formula.code); + if (formulaValue === Infinity) { + formulaError = "" + formulaValue = MAX_STOP_LOSS_VALUE + if (stopLossStage === 'Manage Stage') { + formulaError = "WARNING: Formula evaluates to Infinity." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> manageStage.stopLoss -> MAX_STOP_LOSS_VALUE -> formulaError = " + formulaError); } + } + } + } catch (err) { + if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) { + /* + We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined. + */ + } else { + formulaError = err.message + } + } + if (isNaN(formulaValue)) { formulaValue = 0; } + if (formulaValue < MIN_STOP_LOSS_VALUE) { + formulaValue = MIN_STOP_LOSS_VALUE + if (stopLossStage === 'Manage Stage') { + formulaError = "WARNING: Formula is evaluating below the MIN_STOP_LOSS_VALUE." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> manageStage.stopLoss -> MIN_STOP_LOSS_VALUE -> formulaError = " + formulaError); } + } + } + + formulasErrors.push('"' + formulaError + '"') + formulasValues.push(formulaValue) + let key = j + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + p; + formulas.set(key, formulaValue) + } + + /* next phase event */ + let nextPhaseEvent = phase.nextPhaseEvent; + if (nextPhaseEvent !== undefined) { + + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + + let situation = nextPhaseEvent.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + p + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + } + } + + if (manageStage.takeProfit !== undefined) { + + for (let p = 0; p < manageStage.takeProfit.phases.length; p++) { + + let phase = manageStage.takeProfit.phases[p]; + + /* Evaluate Formula */ + let formulaValue + let formulaError = '' + + if (phase.formula !== undefined) { + try { + formulaValue = eval(phase.formula.code); + if (formulaValue === Infinity) { + formulaError = "Formula evaluates to Infinity." + formulaValue = MAX_TAKE_PROFIT_VALUE + if (takeProfitStage === 'Manage Stage') { + formulaError = "WARNING: Formula evaluates to Infinity." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> manageStage.takeProfit -> MAX_TAKE_PROFIT_VALUE -> formulaError = " + formulaError); } + } + } + } catch (err) { + if (phase.formula.code.indexOf('previous') > 0 && err.message.indexOf('of undefined') > 0) { + /* + We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined. + */ + } else { + formulaError = err.message + } + } + if (isNaN(formulaValue)) { formulaValue = 0; } + if (formulaValue < MIN_TAKE_PROFIT_VALUE) { + formulaValue = MIN_TAKE_PROFIT_VALUE + if (takeProfitStage === 'Manage Stage') { + formulaError = "WARNING: Formula is evaluating below the MIN_TAKE_PROFIT_VALUE." + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[WARN] runSimulation -> loop -> manageStage.takeProfit -> MIN_TAKE_PROFIT_VALUE -> formulaError = " + formulaError); } + } + } + + formulasErrors.push('"' + formulaError + '"') + formulasValues.push(formulaValue) + let key = j + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + p; + formulas.set(key, formulaValue) + } + + /* next phase event */ + let nextPhaseEvent = phase.nextPhaseEvent; + if (nextPhaseEvent !== undefined) { + + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + + let situation = nextPhaseEvent.situations[k]; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + p + '-' + k + '-' + m; + + if (condition.code !== undefined) { + newCondition(key, condition.code); + } + } + } + } + } + } + } + + function newCondition(key, node) { + + let condition; + let error = '' + let value + + try { + value = eval(node.code); + } catch (err) { + /* + One possible error is that the conditions references a .previous that is undefined. For this + reason and others, we will simply set the value to false. + */ + value = false + + if (node.code.indexOf('previous') > -1 && err.message.indexOf('of undefined') > -1 || + node.code.indexOf('chart') > -1 && err.message.indexOf('of undefined') > -1 + ) { + /* + We are not going to set an error for the casess we are using previous and the error is that the indicator is undefined. + */ + } else { + node.error = err.message + " @ " + (new Date(candle.begin)).toLocaleString() + } + } + + condition = { + key: key, + value: value + }; + + conditions.set(condition.key, condition); + + if (condition.value) { + conditionsArrayValues.push(1); + } else { + conditionsArrayValues.push(0); + } + } + } + + /* Trigger On Conditions */ + if ( + strategyStage === 'No Stage' && + currentStrategyIndex === -1 + ) { + let minimumBalance + let maximumBalance + let balance + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + balance = balanceAssetA + minimumBalance = bot.VALUES_TO_USE.minimumBalanceA + maximumBalance = bot.VALUES_TO_USE.maximumBalanceA + } else { + balance = balanceAssetB + minimumBalance = bot.VALUES_TO_USE.minimumBalanceB + maximumBalance = bot.VALUES_TO_USE.maximumBalanceB + } + + if (balance > minimumBalance && balance < maximumBalance) { + + /* + Here we need to pick a strategy, or if there is not suitable strategy for the current + market conditions, we pass until the next period. + + To pick a new strategy we will evaluate what we call the trigger on. Once we enter + into one strategy, we will ignore market conditions for others. However there is also + a strategy trigger off which can be hit before taking a position. If hit, we would + be outside a strategy again and looking for the condition to enter all over again. + + */ + + for (let j = 0; j < tradingSystem.strategies.length; j++) { + + let strategy = tradingSystem.strategies[j]; + + let triggerStage = strategy.triggerStage + + if (triggerStage !== undefined) { + + if (triggerStage.triggerOn !== undefined) { + + for (let k = 0; k < triggerStage.triggerOn.situations.length; k++) { + + let situation = triggerStage.triggerOn.situations[k]; + let passed = true; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + 'triggerStage' + '-' + 'triggerOn' + '-' + k + '-' + m; + + let value = false + if (conditions.get(key) !== undefined) { + value = conditions.get(key).value; + } + + if (value === false) { passed = false; } + } + + if (passed) { + + strategyStage = 'Trigger Stage'; + currentStrategyIndex = j; + currentStrategy.begin = candle.begin; + currentStrategy.beginRate = candle.min; + currentStrategy.endRate = candle.min; // In case the strategy does not get exited + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.strategyStage = strategyStage; + yesterday.currentStrategyIndex = currentStrategyIndex; + yesterday.currentStrategy.begin = currentStrategy.begin; + yesterday.currentStrategy.beginRate = currentStrategy.beginRate; + yesterday.currentStrategy.endRate = currentStrategy.endRate; + } + } + + distanceToLast.triggerOn = 1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.triggerOn = distanceToLast.triggerOn + } + } + + checkAnnouncements(triggerStage.triggerOn) + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Switching to Trigger Stage because conditions at Trigger On Event were met."); } + break; + } + } + } + } + } + } else { + let stopRunningDate = new Date(candle.begin) + if (balance <= minimumBalance) { + tradingSystem.error = "Min Balance @ " + stopRunningDate.toLocaleString() + } + if (balance >= maximumBalance) { + tradingSystem.error = "Max Balance @ " + stopRunningDate.toLocaleString() + } + } + } + + /* Trigger Off Condition */ + if (strategyStage === 'Trigger Stage') { + + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + let triggerStage = strategy.triggerStage + + if (triggerStage !== undefined) { + + if (triggerStage.triggerOff !== undefined) { + + for (let k = 0; k < triggerStage.triggerOff.situations.length; k++) { + + let situation = triggerStage.triggerOff.situations[k]; + let passed = true; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = currentStrategyIndex + '-' + 'triggerStage' + '-' + 'triggerOff' + '-' + k + '-' + m; + + let value = false + if (conditions.get(key) !== undefined) { + value = conditions.get(key).value; + } + + if (value === false) { passed = false; } + } + + if (passed) { + + currentStrategy.number = currentStrategyIndex + currentStrategy.end = candle.end; + currentStrategy.endRate = candle.min; + currentStrategy.status = 1; + strategyStage = 'No Stage'; + currentStrategyIndex = -1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.currentStrategy.number = currentStrategy.number; + yesterday.currentStrategy.end = currentStrategy.end; + yesterday.currentStrategy.endRate = currentStrategy.endRate; + yesterday.currentStrategy.status = currentStrategy.status; + yesterday.strategyStage = strategyStage; + yesterday.currentStrategyIndex = currentStrategyIndex; + } + } + + distanceToLast.triggerOff = 1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.triggerOff = distanceToLast.triggerOff + } + } + + checkAnnouncements(triggerStage.triggerOff) + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Switching to No Stage because conditions at the Trigger Off Event were met."); } + break; + } + } + } + } + } + + /* Take Position Condition */ + if (strategyStage === 'Trigger Stage') { + + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + let triggerStage = strategy.triggerStage + + if (triggerStage !== undefined) { + + if (triggerStage.takePosition !== undefined) { + + for (let k = 0; k < triggerStage.takePosition.situations.length; k++) { + + let situation = triggerStage.takePosition.situations[k]; + let passed = true; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = currentStrategyIndex + '-' + 'triggerStage' + '-' + 'takePosition' + '-' + k + '-' + m; + + let value = false + if (conditions.get(key) !== undefined) { + value = conditions.get(key).value; + } + + if (value === false) { passed = false; } + } + + if (passed) { + + type = '"Take Position"'; + + strategyStage = 'Open Stage'; + stopLossStage = 'Open Stage'; + takeProfitStage = 'Open Stage'; + stopLossPhase = 0; + takeProfitPhase = 0; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.strategyStage = strategyStage; + yesterday.stopLossStage = stopLossStage; + yesterday.takeProfitStage = takeProfitStage; + yesterday.stopLossPhase = stopLossPhase; + yesterday.takeProfitPhase = takeProfitPhase; + } + } + + takePositionNow = true + + checkAnnouncements(triggerStage.takePosition) + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Conditions at the Take Position Event were met."); } + break; + } + } + } + } + } + + /* Stop Loss Management */ + if ( + (strategyStage === 'Open Stage' || strategyStage === 'Manage Stage') && + takePositionNow !== true + ) { + + checkStopPhases() + calculateStopLoss(); + + } + + function checkStopPhases() { + + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + let openStage = strategy.openStage + let manageStage = strategy.manageStage + let parentNode + let j = currentStrategyIndex + let stageKey + let initialDefinitionKey = '' + let p + + if (stopLossStage === 'Open Stage' && openStage !== undefined) { + if (openStage.initialDefinition !== undefined) { + if (openStage.initialDefinition.stopLoss !== undefined) { + parentNode = openStage.initialDefinition + initialDefinitionKey = '-' + 'initialDefinition' + stageKey = 'openStage' + p = stopLossPhase + } + } + } + + if (stopLossStage === 'Manage Stage' && manageStage !== undefined) { + if (manageStage.stopLoss !== undefined) { + parentNode = manageStage + stageKey = 'manageStage' + p = stopLossPhase - 1 + } + } + + let phase = parentNode.stopLoss.phases[p]; + + let nextPhaseEvent = phase.nextPhaseEvent; + if (nextPhaseEvent !== undefined) { + + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + + let situation = nextPhaseEvent.situations[k]; + let passed = true; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + stageKey + initialDefinitionKey + '-' + 'stopLoss' + '-' + p + '-' + k + '-' + m; + + let value = false + if (conditions.get(key) !== undefined) { + value = conditions.get(key).value; + } + + if (value === false) { passed = false; } + } + + if (passed) { + + stopLossPhase++; + stopLossStage = 'Manage Stage' + if (takeProfitPhase > 0) { strategyStage = 'Manage Stage' } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.stopLossPhase = stopLossPhase; + yesterday.stopLossStage = stopLossStage; + yesterday.strategyStage = strategyStage; + } + } + + checkAnnouncements(nextPhaseEvent) + return; + } + } + } + } + + function calculateStopLoss() { + + let strategy = tradingSystem.strategies[currentStrategyIndex]; + let openStage = strategy.openStage + let manageStage = strategy.manageStage + let phase + let key + + if (stopLossStage === 'Open Stage' && openStage !== undefined) { + if (openStage.initialDefinition !== undefined) { + if (openStage.initialDefinition.stopLoss !== undefined) { + phase = openStage.initialDefinition.stopLoss.phases[stopLossPhase]; + key = currentStrategyIndex + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'stopLoss' + '-' + (stopLossPhase); + } + } + } + + if (stopLossStage === 'Manage Stage' && manageStage !== undefined) { + if (manageStage.stopLoss !== undefined) { + phase = manageStage.stopLoss.phases[stopLossPhase - 1]; + key = currentStrategyIndex + '-' + 'manageStage' + '-' + 'stopLoss' + '-' + (stopLossPhase - 1); + } + } + + if (phase !== undefined) { + if (phase.formula !== undefined) { + let previousValue = stopLoss + + stopLoss = formulas.get(key) + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.stopLoss = stopLoss + } + } + + if (stopLoss !== previousValue) { + checkAnnouncements(phase, stopLoss) + } + } + } + } + + /* Take Profit Management */ + if ( + (strategyStage === 'Open Stage' || strategyStage === 'Manage Stage') && + takePositionNow !== true + ) { + + checkTakeProfitPhases(); + calculateTakeProfit(); + + } + + function checkTakeProfitPhases() { + + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + let openStage = strategy.openStage + let manageStage = strategy.manageStage + let parentNode + let j = currentStrategyIndex + let stageKey + let initialDefinitionKey = '' + let p + + if (takeProfitStage === 'Open Stage' && openStage !== undefined) { + if (openStage.initialDefinition !== undefined) { + if (openStage.initialDefinition.takeProfit !== undefined) { + parentNode = openStage.initialDefinition + initialDefinitionKey = '-' + 'initialDefinition' + stageKey = 'openStage' + p = takeProfitPhase + } + } + } + + if (takeProfitStage === 'Manage Stage' && manageStage !== undefined) { + if (manageStage.takeProfit !== undefined) { + parentNode = manageStage + stageKey = 'manageStage' + p = takeProfitPhase - 1 + } + } + + let phase = parentNode.takeProfit.phases[p]; + + let nextPhaseEvent = phase.nextPhaseEvent; + if (nextPhaseEvent !== undefined) { + + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + + let situation = nextPhaseEvent.situations[k]; + let passed = true; + + for (let m = 0; m < situation.conditions.length; m++) { + + let condition = situation.conditions[m]; + let key = j + '-' + stageKey + initialDefinitionKey + '-' + 'takeProfit' + '-' + p + '-' + k + '-' + m; + + let value = false + if (conditions.get(key) !== undefined) { + value = conditions.get(key).value; + } + + if (value === false) { passed = false; } + } + + if (passed) { + + takeProfitPhase++; + takeProfitStage = 'Manage Stage' + if (stopLossPhase > 0) { strategyStage = 'Manage Stage' } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.takeProfitPhase = takeProfitPhase; + yesterday.takeProfitStage = takeProfitStage; + yesterday.strategyStage = strategyStage; + } + } + + checkAnnouncements(nextPhaseEvent) + return; + } + } + } + } + + function calculateTakeProfit() { + + let strategy = tradingSystem.strategies[currentStrategyIndex]; + let openStage = strategy.openStage + let manageStage = strategy.manageStage + let phase + let key + + if (takeProfitStage === 'Open Stage' && openStage !== undefined) { + if (openStage.initialDefinition !== undefined) { + if (openStage.initialDefinition.takeProfit !== undefined) { + phase = openStage.initialDefinition.takeProfit.phases[takeProfitPhase]; + key = currentStrategyIndex + '-' + 'openStage' + '-' + 'initialDefinition' + '-' + 'takeProfit' + '-' + (takeProfitPhase); + } + } + } + + if (takeProfitStage === 'Manage Stage' && manageStage !== undefined) { + if (manageStage.takeProfit !== undefined) { + phase = manageStage.takeProfit.phases[takeProfitPhase - 1]; + key = currentStrategyIndex + '-' + 'manageStage' + '-' + 'takeProfit' + '-' + (takeProfitPhase - 1); + } + } + + if (phase !== undefined) { + if (phase.formula !== undefined) { + + let previousValue = stopLoss + + takeProfit = formulas.get(key) + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.takeProfit = takeProfit + } + } + + if (takeProfit !== previousValue) { + checkAnnouncements(phase, takeProfit) + } + } + } + } + + /* Keeping Position Counters Up-to-date */ + if ( + (strategyStage === 'Open Stage' || strategyStage === 'Manage Stage') + ) { + + if (takePositionNow === true) { + positionPeriods = 0 + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.positionPeriods = 0 + } + } + } + + positionPeriods++; + positionDays = positionPeriods * timePeriod / ONE_DAY_IN_MILISECONDS; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.positionPeriods = positionPeriods + } + } + } else { + positionPeriods = 0 + positionDays = 0 + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.positionPeriods = 0 + } + } + } + + /* Keeping Distance Counters Up-to-date */ + if ( + distanceToLast.triggerOn > 0 // with this we avoind counting before the first event happens. + ) { + distanceToLast.triggerOn++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.triggerOn = distanceToLast.triggerOn + } + } + } + + if ( + distanceToLast.triggerOff > 0 // with this we avoind counting before the first event happens. + ) { + distanceToLast.triggerOff++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.triggerOff = distanceToLast.triggerOff + } + } + } + + if ( + distanceToLast.takePosition > 0 // with this we avoind counting before the first event happens. + ) { + distanceToLast.takePosition++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.takePosition = distanceToLast.takePosition + } + } + } + + if ( + distanceToLast.closePosition > 0 // with this we avoind counting before the first event happens. + ) { + distanceToLast.closePosition++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.takePosition = distanceToLast.closePosition + } + } + } + + /* Checking if Stop or Take Profit were hit */ + if ( + (strategyStage === 'Open Stage' || strategyStage === 'Manage Stage') && + takePositionNow !== true + ) { + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + /* Checking what happened since the last execution. We need to know if the Stop Loss + or our Take Profit were hit. */ + + /* Stop Loss condition: Here we verify if the Stop Loss was hitted or not. */ + + if ((bot.VALUES_TO_USE.baseAsset === 'BTC' && candle.max >= stopLoss) || (bot.VALUES_TO_USE.baseAsset !== 'BTC' && candle.min <= stopLoss)) { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Stop Loss was hit."); } + /* + Hit Point Validation + + This prevents misscalculations when a formula places the stop loss in this case way beyond the market price. + If we take the stop loss value at those situation would be a huge distortion of facts. + */ + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + if (stopLoss < candle.min) { + stopLoss = candle.min + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.stopLoss = stopLoss + } + } + } + } else { + if (stopLoss > candle.max) { + stopLoss = candle.max + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.stopLoss = stopLoss + } + } + } + } + + let slippedStopLoss = stopLoss + + /* Apply the Slippage */ + let slippageAmount = slippedStopLoss * bot.VALUES_TO_USE.slippage.stopLoss / 100 + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + slippedStopLoss = slippedStopLoss + slippageAmount + } else { + slippedStopLoss = slippedStopLoss - slippageAmount + } + + let finalStopLoss = slippedStopLoss; + + let feePaid = 0 + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + strategy.positionSize = balanceAssetB / finalStopLoss; + strategy.positionRate = finalStopLoss; + + feePaid = balanceAssetB / finalStopLoss * bot.VALUES_TO_USE.feeStructure.taker / 100 + + balanceAssetA = balanceAssetA + balanceAssetB / finalStopLoss - feePaid; + balanceAssetB = 0; + } else { + strategy.positionSize = balanceAssetA * finalStopLoss; + strategy.positionRate = finalStopLoss; + + feePaid = balanceAssetA * finalStopLoss * bot.VALUES_TO_USE.feeStructure.taker / 100 + + balanceAssetB = balanceAssetB + balanceAssetA * finalStopLoss - feePaid; + balanceAssetA = 0; + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.balanceAssetA = balanceAssetA; + yesterday.balanceAssetB = balanceAssetB; + } + } + + marketRate = finalStopLoss; + type = '"Close@StopLoss"'; + strategyStage = 'Close Stage'; + stopLossStage = 'No Stage'; + takeProfitStage = 'No Stage'; + currentTrade.end = candle.end; + currentTrade.status = 1; + currentTrade.exitType = 1; + currentTrade.endRate = finalStopLoss; + + closePositionNow = true; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.strategyStage = strategyStage; + yesterday.stopLossStage = stopLossStage; + yesterday.takeProfitStage = takeProfitStage; + yesterday.currentTrade.end = currentTrade.end; + yesterday.currentTrade.status = currentTrade.status; + yesterday.currentTrade.exitType = currentTrade.exitType; + yesterday.currentTrade.endRate = currentTrade.endRate; + } + } + } + + /* Take Profit condition: Here we verify if the Take Profit was hit or not. */ + + if ((bot.VALUES_TO_USE.baseAsset === 'BTC' && candle.min <= takeProfit) || (bot.VALUES_TO_USE.baseAsset !== 'BTC' && candle.max >= takeProfit)) { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Take Profit was hit."); } + /* + Hit Point Validation: + + This prevents misscalculations when a formula places the take profit in this case way beyond the market price. + If we take the stop loss value at those situation would be a huge distortion of facts. + */ + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + if (takeProfit > candle.max) { + takeProfit = candle.max + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.takeProfit = takeProfit + } + } + } + } else { + if (takeProfit < candle.min) { + takeProfit = candle.min + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.takeProfit = takeProfit + } + } + } + } + + let slippedTakeProfit = takeProfit + /* Apply the Slippage */ + let slippageAmount = slippedTakeProfit * bot.VALUES_TO_USE.slippage.takeProfit / 100 + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + slippedTakeProfit = slippedTakeProfit + slippageAmount + } else { + slippedTakeProfit = slippedTakeProfit - slippageAmount + } + + let finalTakeProfit = slippedTakeProfit; + + let feePaid = 0 + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + strategy.positionSize = balanceAssetB / finalTakeProfit; + strategy.positionRate = finalTakeProfit; + + feePaid = balanceAssetB / finalTakeProfit * bot.VALUES_TO_USE.feeStructure.taker / 100 + + balanceAssetA = balanceAssetA + balanceAssetB / finalTakeProfit - feePaid; + balanceAssetB = 0; + } else { + strategy.positionSize = balanceAssetA * finalTakeProfit; + strategy.positionRate = finalTakeProfit; + + feePaid = balanceAssetA * finalTakeProfit * bot.VALUES_TO_USE.feeStructure.taker / 100 + + balanceAssetB = balanceAssetB + balanceAssetA * finalTakeProfit - feePaid; + balanceAssetA = 0; + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.balanceAssetA = balanceAssetA; + yesterday.balanceAssetB = balanceAssetB; + + } + } + + marketRate = finalTakeProfit; + type = '"Close@TakeProfit"'; + strategyStage = 'Close Stage'; + stopLossStage = 'No Stage'; + takeProfitStage = 'No Stage'; + + currentTrade.end = candle.end; + currentTrade.status = 1; + currentTrade.exitType = 2; + currentTrade.endRate = finalTakeProfit; + + closePositionNow = true; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.strategyStage = strategyStage; + yesterday.stopLossStage = stopLossStage; + yesterday.takeProfitStage = takeProfitStage; + + yesterday.currentTrade.end = currentTrade.end; + yesterday.currentTrade.status = currentTrade.status; + yesterday.currentTrade.exitType = currentTrade.exitType; + yesterday.currentTrade.endRate = currentTrade.endRate; + } + } + } + } + + /* Taking a Position */ + if ( + takePositionNow === true + ) { + takePositionNow = false + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionNow -> Entering code block."); } + + /* Inicializing this counter */ + distanceToLast.takePosition = 1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.takePosition = distanceToLast.takePosition + } + } + + /* Position size and rate */ + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + tradePositionSize = strategy.positionSize; + tradePositionRate = strategy.positionRate; + + /* We take what was calculated at the formula and apply the slippage. */ + let slippageAmount = tradePositionRate * bot.VALUES_TO_USE.slippage.positionRate / 100 + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + tradePositionRate = tradePositionRate - slippageAmount + } else { + tradePositionRate = tradePositionRate + slippageAmount + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.tradePositionSize = tradePositionSize; + yesterday.tradePositionRate = tradePositionRate; + } + } + + /* Update the trade record information. */ + currentTrade.begin = candle.begin; + currentTrade.beginRate = tradePositionRate; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.currentTrade.begin = currentTrade.begin; + yesterday.currentTrade.beginRate = currentTrade.beginRate; + } + } + + /* Check if we need to execute. */ + if (i > candles.length - 10) { /* Only at the last candles makes sense to check if we are in live mode or not.*/ + /* Check that we are in LIVE MODE */ + if (bot.startMode === "Live") { + /* We see if we need to put the actual order at the exchange. */ + if (interExecutionMemory.executionContext !== undefined) { + switch (interExecutionMemory.executionContext.status) { + case "Without a Position": { // We need to put the order because It was not put yet. + if (strategy.openStage !== undefined) { + if (strategy.openStage.openExecution !== undefined) { + putOpeningOrder() + return + } + } + break + } + case "Position Closed": { // Waiting for a confirmation that the position was closed. + if (strategy.openStage !== undefined) { + if (strategy.openStage.openExecution !== undefined) { + putOpeningOrder() + return + } + } + break + } + case "Taking Position": { // Waiting for a confirmation that the position was taken. + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionNow -> Exiting code block because status is Taking Position."); } + break + } + case "In a Position": { // This should mean that we already put the order at the exchange. + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionNow -> Exiting code block because status is In a Position."); } + break + } + } + } else { // The context does not exist so it means we are not in a position. + if (strategy.openStage !== undefined) { + if (strategy.openStage.openExecution !== undefined) { + putOpeningOrder() + return + } + } + } + } else { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionNow -> Not trading live."); } + } + } else { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionNow -> Not the last closed candle."); } + } + + takePositionAtSimulation() + return + + function putOpeningOrder() { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> Entering function."); } + + /* We wont take a position unless we are withing the bot.VALUES_TO_USE.timeRange.initialDatetime and the bot.VALUES_TO_USE.timeRange.finalDatetime range */ + if (bot.VALUES_TO_USE.timeRange.initialDatetime !== undefined) { + if (candle.end < bot.VALUES_TO_USE.timeRange.initialDatetime.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because current candle ends before the start date. -> bot.VALUES_TO_USE.timeRange.initialDatetime = " + bot.VALUES_TO_USE.timeRange.initialDatetime); } + takePositionAtSimulation() + return; + } + } + + /*We wont take a position if we are past the final datetime */ + if (bot.VALUES_TO_USE.timeRange.finalDatetime !== undefined) { + if (candle.begin > bot.VALUES_TO_USE.timeRange.finalDatetime.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> putOpeningOrder -> Not placing the trade at the exchange because current candle begins after the end date. -> bot.VALUES_TO_USE.timeRange.finalDatetime = " + bot.VALUES_TO_USE.timeRange.finalDatetime); } + takePositionAtSimulation() + return; + } + } + + + /* Checking the status of current positions */ + let positions = assistant.getPositions(); + if (positions.length > 0) { + let position = positions[positions.length - 1] // We are allways checking the the last position is not open. + if (position.status === 'open') { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because the last position is still open. "); } + afterLoop(); + return + } + } + + /* Mechanism to avoid putting the same order over and over again at different executions of the simulation engine. */ + + if (interExecutionMemory.executionContext !== undefined) { + if (interExecutionMemory.executionContext.periods !== undefined) { + if (periods <= interExecutionMemory.executionContext.periods) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because it was already placed at a previous execution."); } + takePositionAtSimulation() + return; + } + } + } + + /* We are not going to place orders based on outdated information. The next filter prevents firing orders when backtesting. */ + if (currentDay) { + let today = new Date(Math.trunc((new Date().valueOf()) / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS) + let processDay = new Date(Math.trunc(currentDay.valueOf() / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS) + if (today.valueOf() !== processDay.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> Not placing the trade at the exchange because the current candle belongs to the previous day and that is considered simulation and not live trading."); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> today = " + today); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> processDay = " + processDay); } + takePositionAtSimulation() + return; + } + } + + let openPositionRate + let amountA + let amountB + let positionDirection + let availableBalance = assistant.getAvailableBalance() + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + positionDirection = "sell" + + openPositionRate = tradePositionRate - 100 // 100 is Provisional since we do have management of orders yet + + amountA = tradePositionSize * openPositionRate + amountB = tradePositionSize + + if (amountB > availableBalance.assetB) { // The assistant know what fees were paid. + amountB = availableBalance.assetB + } + } else { + positionDirection = "buy" + + openPositionRate = tradePositionRate + 100 // 100 is Provisional since we do have management of orders yet + + amountA = tradePositionSize + amountB = tradePositionSize / openPositionRate + + if (amountA > availableBalance.assetA) { // The assistant know what fees were paid. + amountA = availableBalance.assetA + } + } + + interExecutionMemory.executionContext = { + status: "Taking Position", + periods: periods, + } + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> Ready to put order."); } + assistant.putPosition(positionDirection, openPositionRate, amountA, amountB, onOrderPut) + + function onOrderPut(err) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> Entering function."); } + + try { + switch (err.result) { + case global.DEFAULT_OK_RESPONSE.result: { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> DEFAULT_OK_RESPONSE "); } + interExecutionMemory.executionContext = { + status: "In a Position", + periods: periods, + amountA: amountA, + amountB: amountB + } + takePositionAtSimulation() + return; + } + case global.DEFAULT_FAIL_RESPONSE.result: { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> DEFAULT_FAIL_RESPONSE "); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> Message = " + err.message); } + strategy.openStage.openExecution.error = err.message + afterLoop() + return; + } + case global.DEFAULT_RETRY_RESPONSE.result: { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> DEFAULT_RETRY_RESPONSE "); } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> Message = " + err.message); } + strategy.openStage.openExecution.error = err.message + afterLoop() + return; + } + } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> Unexpected Response -> Message = " + err.message); } + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + + } catch (err) { + logger.write(MODULE_NAME, "[ERROR] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> err = " + err.stack); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } + } + } + + function takePositionAtSimulation() { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionAtSimulation -> Entering function."); } + + /* Continue with the simulation */ + calculateTakeProfit(); + calculateStopLoss(); + + marketRate = candle.close; + + previousBalanceAssetA = balanceAssetA; + previousBalanceAssetB = balanceAssetB; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.previousBalanceAssetA = previousBalanceAssetA; + yesterday.previousBalanceAssetB = previousBalanceAssetB; + } + } + + lastTradeProfitLoss = 0; + lastTradeROI = 0; + + let feePaid = 0 + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + + feePaid = tradePositionSize * tradePositionRate * bot.VALUES_TO_USE.feeStructure.taker / 100 + + balanceAssetB = balanceAssetB + tradePositionSize * tradePositionRate - feePaid; + balanceAssetA = balanceAssetA - tradePositionSize; + } else { + + feePaid = tradePositionSize / tradePositionRate * bot.VALUES_TO_USE.feeStructure.taker / 100 + + balanceAssetA = balanceAssetA + tradePositionSize / tradePositionRate - feePaid; + balanceAssetB = balanceAssetB - tradePositionSize; + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.balanceAssetA = balanceAssetA; + yesterday.balanceAssetB = balanceAssetB; + + yesterday.lastTradeProfitLoss = lastTradeProfitLoss; + yesterday.lastTradeROI = lastTradeROI; + } + } + + addRecord(); + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> takePositionAtSimulation -> Exiting Loop Body after taking position at simulation."); } + controlLoop(); + return + } + } + + if (closePositionNow === true) { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Closing a Position -> Entering code block."); } + + closePositionNow = false + + /* Inicializing this counter */ + distanceToLast.closePosition = 1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.closePosition = distanceToLast.closePosition + } + } + + /* Position size and rate */ + let strategy = tradingSystem.strategies[currentStrategyIndex]; + + if (i > candles.length - 10) { /* Only at the last candles makes sense to check if we are in live mode or not.*/ + /* Check that we are in LIVE MODE */ + if (bot.startMode === "Live") { + /* We see if we need to put the actual order at the exchange. */ + if (interExecutionMemory.executionContext !== undefined) { + switch (interExecutionMemory.executionContext.status) { + case "Without a Position": { // No way to close anything at the exchange. + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Closing a Position -> Exiting code block because status is Without a Position."); } + break + } + case "In a Position": { // This should mean that we already put the order at the exchange. + if (strategy.closeStage !== undefined) { + if (strategy.closeStage.closeExecution !== undefined) { + putClosingOrder() + return + } + } + break + } + case "Closing Position": { // Waiting for a confirmation that the position was taken. + if (strategy.closeStage !== undefined) { + if (strategy.closeStage.closeExecution !== undefined) { + putClosingOrder() + return + } + } + break + } + + case "Position Closed": { // + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Closing a Position -> Exiting code block because status is Position Closed."); } + break + } + } + } + } + } else { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Closing a Position -> Not within the last 10 candles."); } + } + + closePositionAtSimulation() + return + + function putClosingOrder() { + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> putClosingOrder -> Entering function."); } + + /* Checking the status of current positions */ + let positions = assistant.getPositions(); + if (positions.length > 0) { + let position = positions[positions.length - 1] // We are allways checking the the last position is not open. + if (position.status === 'open') { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putClosingOrder -> Exiting function because status of last position is Open."); } + afterLoop(); + return + } + } + + /* Mechanism to avoid putting the same order over and over again at different executions of the simulation engine. */ + + if (interExecutionMemory.executionContext !== undefined) { + if (interExecutionMemory.executionContext.periods !== undefined) { + if (periods <= interExecutionMemory.executionContext.periods) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putClosingOrder -> Exiting function because this closing was already submited at a previous execution."); } + closePositionAtSimulation() + return; + } + } + } + + let closePositionRate + let amountA + let amountB + let positionDirection + let availableBalance = assistant.getAvailableBalance() + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + positionDirection = "buy" + + closePositionRate = ticker.last + 100; // This is provisional and totally arbitrary, until we have a formula on the designer that defines this stuff. + + amountA = availableBalance.assetA + amountB = availableBalance.assetA / closePositionRate + + } else { + positionDirection = "sell" + + closePositionRate = ticker.last - 100; // This is provisional and totally arbitrary, until we have a formula on the designer that defines this stuff. + + amountA = availableBalance.assetB * closePositionRate + amountB = availableBalance.assetB + + } + + interExecutionMemory.executionContext = { + status: "Closing Position", + periods: periods, + } + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putClosingOrder -> About to close position at the exchange."); } + assistant.putPosition(positionDirection, closePositionRate, amountA, amountB, onOrderPut) + + function onOrderPut(err) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putClosingOrder -> onOrderPut -> Entering function."); } + + try { + switch (err.result) { + case global.DEFAULT_OK_RESPONSE.result: { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putClosingOrder -> onOrderPut -> DEFAULT_OK_RESPONSE "); } + interExecutionMemory.executionContext = { + status: "Position Closed", + periods: periods, + amountA: amountA, + amountB: amountB + } + closePositionAtSimulation() + return; + } + case global.DEFAULT_FAIL_RESPONSE.result: { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putClosingOrder -> onOrderPut -> DEFAULT_FAIL_RESPONSE "); } + /* We will assume that the problem is temporary, and expect that it will work at the next execution.*/ + strategy.closeStage.closeExecution.error = err.message + afterLoop() + return; + } + case global.DEFAULT_RETRY_RESPONSE.result: { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> putOpeningOrder -> onOrderPut -> DEFAULT_RETRY_RESPONSE "); } + strategy.closeStage.closeExecution.error = err.message + afterLoop() + return; + } + } + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[ERROR] runSimulation -> loop -> putClosingOrder -> onOrderPut -> Unexpected Response -> Message = " + err.message); } + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + + } catch (err) { + logger.write(MODULE_NAME, "[ERROR] runSimulation -> loop -> putClosingOrder -> onOrderPut -> err = " + err.stack); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } + } + } + + function closePositionAtSimulation() { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> closePositionAtSimulation -> Entering function."); } + + roundtrips++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.roundtrips = roundtrips + } + } + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + lastTradeProfitLoss = balanceAssetA - previousBalanceAssetA; + lastTradeROI = lastTradeProfitLoss * 100 / tradePositionSize; + if (isNaN(lastTradeROI)) { lastTradeROI = 0; } + profit = balanceAssetA - bot.VALUES_TO_USE.initialBalanceA; + } else { + lastTradeProfitLoss = balanceAssetB - previousBalanceAssetB; + lastTradeROI = lastTradeProfitLoss * 100 / tradePositionSize; + if (isNaN(lastTradeROI)) { lastTradeROI = 0; } + profit = balanceAssetB - bot.VALUES_TO_USE.initialBalanceB; + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.lastTradeProfitLoss = lastTradeProfitLoss; + yesterday.profit = profit; + yesterday.lastTradeROI = lastTradeROI; + } + } + + currentTrade.lastTradeROI = lastTradeROI; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.currentTrade.lastTradeROI = currentTrade.lastTradeROI; + } + } + + if (lastTradeProfitLoss > 0) { + hits++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.hits = hits + } + } + + } else { + fails++; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.fails = fails + } + } + } + + if (bot.VALUES_TO_USE.baseAsset === 'BTC') { + ROI = (bot.VALUES_TO_USE.initialBalanceA + profit) / bot.VALUES_TO_USE.initialBalanceA - 1; + hitRatio = hits / roundtrips; + anualizedRateOfReturn = ROI / days * 365; + } else { + ROI = (bot.VALUES_TO_USE.initialBalanceB + profit) / bot.VALUES_TO_USE.initialBalanceB - 1; + hitRatio = hits / roundtrips; + anualizedRateOfReturn = ROI / days * 365; + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.ROI = ROI; + yesterday.hitRatio = hitRatio; + yesterday.anualizedRateOfReturn = anualizedRateOfReturn; + } + } + + addRecord(); + + stopLoss = 0; + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.stopLoss = stopLoss + } + } + takeProfit = 0; + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.takeProfit = takeProfit + } + } + + tradePositionRate = 0; + tradePositionSize = 0; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.tradePositionSize = tradePositionSize; + yesterday.tradePositionRate = tradePositionRate; + } + } + + timerToCloseStage = candle.begin + stopLossStage = 'No Stage'; + takeProfitStage = 'No Stage'; + stopLossPhase = -1; + takeProfitPhase = -1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.stopLossStage = stopLossStage; + yesterday.takeProfitStage = takeProfitStage; + yesterday.stopLossPhase = stopLossPhase; + yesterday.takeProfitPhase = takeProfitPhase; + } + } + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> closePositionAtSimulation -> Exiting Loop Body after closing position at simulation."); } + controlLoop(); + return + } + } + + /* Closing the Closing Stage */ + if (strategyStage === 'Close Stage') { + if (candle.begin - 5 * 60 * 1000 > timerToCloseStage) { + + currentStrategy.number = currentStrategyIndex + currentStrategy.end = candle.end; + currentStrategy.endRate = candle.min; + currentStrategy.status = 1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + + yesterday.currentStrategy.number = currentStrategy.number; + yesterday.currentStrategy.end = currentStrategy.end; + yesterday.currentStrategy.endRate = currentStrategy.endRate; + yesterday.currentStrategy.status = currentStrategy.status; + } + } + + currentStrategyIndex = -1; + strategyStage = 'No Stage'; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.currentStrategyIndex = currentStrategyIndex; + yesterday.strategyStage = strategyStage; + } + } + + timerToCloseStage = 0 + + distanceToLast.triggerOff = 1; + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.distanceToLast.triggerOff = distanceToLast.triggerOff + } + } + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Closing the Closing Stage -> Exiting Close Stage."); } + } else { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Closing the Closing Stage -> Waiting for timer."); } + } + } + + /* Not a buy or sell condition */ + + marketRate = candle.close; + addRecord(); + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> Exiting Loop Body after adding a record."); } + controlLoop(); + return + + function addRecord() { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> loop -> addRecord -> Entering function."); } + + // Since we are going to write the message to a file that the Simulation Executor is going to read, we use the abbreviations. + let messageType; + let message; + let simulationRecord; + let executionRecord; + + let strategyStageNumber + switch (strategyStage) { + case 'No Stage': { + strategyStageNumber = 0 + break + } + case 'Trigger Stage': { + strategyStageNumber = 1 + break + } + case 'Open Stage': { + strategyStageNumber = 2 + break + } + case 'Manage Stage': { + strategyStageNumber = 3 + break + } + case 'Close Stage': { + strategyStageNumber = 4 + break + } + } + + if (balanceAssetA === Infinity) { + balanceAssetA = Number.MAX_SAFE_INTEGER + } + + if (balanceAssetB === Infinity) { + balanceAssetB = Number.MAX_SAFE_INTEGER + } + + let quotedBaseAsset = '"' + bot.VALUES_TO_USE.baseAsset + '"' + + simulationRecord = { + begin: candle.begin, + end: candle.end, + type: type, + marketRate: marketRate, + amount: 1, + balanceA: balanceAssetA, + balanceB: balanceAssetB, + profit: profit, + lastTradeProfitLoss: lastTradeProfitLoss, + stopLoss: stopLoss, + roundtrips: roundtrips, + hits: hits, + fails: fails, + hitRatio: hitRatio, + ROI: ROI, + periods: periods, + days: days, + anualizedRateOfReturn: anualizedRateOfReturn, + positionRate: tradePositionRate, + lastTradeROI: lastTradeROI, + strategy: currentStrategyIndex, + strategyStageNumber: strategyStageNumber, + takeProfit: takeProfit, + stopLossPhase: stopLossPhase, + takeProfitPhase: takeProfitPhase, + executionRecord: '', + positionSize: tradePositionSize, + initialBalanceA: bot.VALUES_TO_USE.initialBalanceA, + minimumBalanceA: bot.VALUES_TO_USE.minimumBalanceA, + maximumBalanceA: bot.VALUES_TO_USE.maximumBalanceA, + initialBalanceB: bot.VALUES_TO_USE.initialBalanceB, + minimumBalanceB: bot.VALUES_TO_USE.minimumBalanceB, + maximumBalanceB: bot.VALUES_TO_USE.maximumBalanceB, + baseAsset: quotedBaseAsset, + positionPeriods: positionPeriods, + positionDays: positionDays, + distanceToLastTriggerOn: distanceToLast.triggerOn, + distanceToLastTriggerOff: distanceToLast.triggerOff, + distanceToLastTakePosition: distanceToLast.takePosition, + distanceToLastClosePosition: distanceToLast.closePosition + } + + recordsArray.push(simulationRecord); + + type = '""'; + + /* Prepare the information for the Conditions File */ + + conditionsArrayRecord.push(currentStrategyIndex); + conditionsArrayRecord.push(strategyStageNumber); + conditionsArrayRecord.push(stopLossPhase); + conditionsArrayRecord.push(takeProfitPhase); + conditionsArrayRecord.push(conditionsArrayValues); + conditionsArrayRecord.push(formulasErrors); + conditionsArrayRecord.push(formulasValues); + + conditionsArray.push(conditionsArrayRecord); + + /* Prepare the information for the Strategies File*/ + + if ( + (currentStrategy.begin !== 0 && currentStrategy.end !== 0) || + (currentStrategy.begin !== 0 && i === candles.length - 1 && lastCandle.end !== lastInstantOfTheDay) + ) { + + strategiesArray.push(currentStrategy); + + currentStrategy = { + begin: 0, + end: 0, + status: 0, + number: 0, + beginRate: 0, + endRate: 0 + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.currentStrategy = { + begin: 0, + end: 0, + status: 0, + number: 0, + beginRate: 0, + endRate: 0 + } + } + } + } + + /* Prepare the information for the Trades File */ + + if ( + (currentTrade.begin !== 0 && currentTrade.end !== 0) || + (currentTrade.begin !== 0 && i === candles.length - 1 && lastCandle.end !== lastInstantOfTheDay) + ) { + + currentTrade.profit = lastTradeProfitLoss; + + tradesArray.push(currentTrade); + + currentTrade = { + begin: 0, + end: 0, + status: 0, + lastTradeROI: 0, + exitType: 0, + beginRate: 0, + endRate: 0 + } + + if (processingDailyFiles) { + if (positionedAtYesterday) { + yesterday.currentTrade = { + begin: 0, + end: 0, + status: 0, + lastTradeROI: 0, + exitType: 0, + beginRate: 0, + endRate: 0 + } + } + } + } + } + + function checkAnnouncements(node, value) { + /* + Value is an optional parameter that represents the value that the announcement is monitoring for change. + If we do receive this value, we will only make the annoucement if the variance is grater than the user pre-defined value + for this variance. + */ + + if (node.announcements !== undefined) { + for (let i = 0; i < node.announcements.length; i++) { + let announcement = node.announcements[i] + let key = node.type + "-" + announcement.name + "-" + announcement.id + + let lastPeriodAnnounced = -1 + let newAnnouncementRecord = {} + + for (let j = 0; j < interExecutionMemory.announcements.length; j++) { + let announcementRecord = interExecutionMemory.announcements[j] + if (announcementRecord.key === key) { + lastPeriodAnnounced = announcementRecord.periods + newAnnouncementRecord = announcementRecord + break + } + } + + if (periods > lastPeriodAnnounced) { + + /* The Value Variation is what tells us how much the value already announced must change in order to annouce it again. */ + let valueVariantion + try { + let code = JSON.parse(announcement.code) + valueVariantion = code.valueVariantion + } catch (err) { + announcement.error = err.message + } + + if (newAnnouncementRecord.value !== undefined && valueVariantion !== undefined) { + let upperLimit = newAnnouncementRecord.value + newAnnouncementRecord.value * valueVariantion / 100 + let lowerLimit = newAnnouncementRecord.value - newAnnouncementRecord.value * valueVariantion / 100 + if (value > lowerLimit && value < upperLimit) { + /* There is not enough variation to announce this again. */ + return + } + } + + /* Here we check if there is a formula attached to the annoucement, we evaluate it to get the annoucement text. */ + let formulaValue + if (announcement.formula !== undefined) { + try { + formulaValue = eval(announcement.formula.code); + } catch (err) { + announcement.formula.error = err.message + } + } + announcement.formulaValue = formulaValue + + bot.SESSION.socialBots.announce(announcement) + + /* Next, we will remmeber this announcement was already done, so that it is not announced again in further processing of the same day. */ + if (newAnnouncementRecord.periods !== undefined) { + newAnnouncementRecord.periods = periods + newAnnouncementRecord.value = value + } else { + newAnnouncementRecord = { + key: key, + periods: periods, + value: value + } + interExecutionMemory.announcements.push(newAnnouncementRecord) + } + } + } + } + } + } + + function controlLoop() { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> controlLoop -> Entering function."); } + + /* Checking if we should continue processing this loop or not.*/ + if (bot.STOP_SESSION === true) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing."); } + console.log("[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing.") + afterLoop() + return + } + + i++ + if (i < candles.length) { + setImmediate(loop) // This will execute the next loop in the next iteration of the NodeJs event loop allowing for other callbacks to be executed. + } else { + afterLoop() + } + } + + function afterLoop() { + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> afterLoop -> Entering function."); } + + /* + Before returning we need to see if we have to record some of our counters at the interExecutionMemory. + To do that, the condition to be met is that this execution must include all candles of the current day. + */ + + if (processingDailyFiles) { + + if (lastCandle.end === lastInstantOfTheDay) { + + interExecutionMemory.stopLoss = yesterday.stopLoss + interExecutionMemory.takeProfit = yesterday.takeProfit + + interExecutionMemory.previousBalanceAssetA = yesterday.previousBalanceAssetA + interExecutionMemory.previousBalanceAssetB = yesterday.previousBalanceAssetB + + interExecutionMemory.tradePositionRate = yesterday.tradePositionRate + interExecutionMemory.tradePositionSize = yesterday.tradePositionSize + + interExecutionMemory.currentStrategyIndex = yesterday.currentStrategyIndex + interExecutionMemory.strategyStage = yesterday.strategyStage + + interExecutionMemory.stopLossPhase = yesterday.stopLossPhase + interExecutionMemory.stopLossStage = yesterday.stopLossStage + + interExecutionMemory.takeProfitPhase = yesterday.takeProfitPhase + interExecutionMemory.takeProfitStage = yesterday.takeProfitStage + + interExecutionMemory.currentStrategy = { + begin: yesterday.currentStrategy.begin, + end: yesterday.currentStrategy.end, + status: yesterday.currentStrategy.status, + number: yesterday.currentStrategy.number, + beginRate: yesterday.currentStrategy.beginRate, + endRate: yesterday.currentStrategy.endRate + } + + interExecutionMemory.currentTrade = { + begin: yesterday.currentTrade.begin, + end: yesterday.currentTrade.end, + status: yesterday.currentTrade.status, + profit: yesterday.currentTrade.profit, + exitType: yesterday.currentTrade.exitType, + beginRate: yesterday.currentTrade.beginRate, + endRate: yesterday.currentTrade.endRate + } + + interExecutionMemory.balanceAssetA = yesterday.balanceAssetA; + interExecutionMemory.balanceAssetB = yesterday.balanceAssetB; + interExecutionMemory.lastTradeProfitLoss = yesterday.lastTradeProfitLoss; + interExecutionMemory.profit = yesterday.profit; + interExecutionMemory.lastTradeROI = yesterday.lastTradeROI; + + interExecutionMemory.roundtrips = yesterday.roundtrips; + interExecutionMemory.fails = yesterday.fails; + interExecutionMemory.hits = yesterday.hits; + interExecutionMemory.periods = yesterday.periods; + interExecutionMemory.positionPeriods = yesterday.positionPeriods; + + interExecutionMemory.distanceToLast.triggerOn = yesterday.distanceToLast.triggerOn + interExecutionMemory.distanceToLast.triggerOff = yesterday.distanceToLast.triggerOff + interExecutionMemory.distanceToLast.takePosition = yesterday.distanceToLast.takePosition + interExecutionMemory.distanceToLast.closePosition = yesterday.distanceToLast.closePosition + + interExecutionMemory.messageId = interExecutionMemory.messageId + yesterday.messageId; + interExecutionMemory.orderId = interExecutionMemory.orderId + yesterday.orderId; + + interExecutionMemory.hitRatio = yesterday.hitRatio; + interExecutionMemory.ROI = yesterday.ROI; + interExecutionMemory.anualizedRateOfReturn = yesterday.anualizedRateOfReturn; + } + } + + if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] runSimulation -> callback -> recordsArray.length = " + recordsArray.length); } + + callback(tradingSystem, recordsArray, conditionsArray, strategiesArray, tradesArray); + } + + function getElement(pArray, currentCandle, datasetName) { + + try { + let element; + for (let i = 0; i < pArray.length; i++) { + element = pArray[i]; + + if (currentCandle.end === element.end) { // when there is an exact match at the end we take that element + return element + } else { + if ( + i > 0 && + element.end > currentCandle.end + ) { + let previousElement = pArray[i - 1] + if (previousElement.end < currentCandle.end) { + return previousElement // If one elements goes into the future of currentCandle, then we stop and take the previous element. + } else { + return + } + } + if ( + i === pArray.length - 1 // If we reach the end of the array, then we return the last element. + && + element.end < currentCandle.end + ) { + return element + } + } + } + return + } + catch (err) { + logger.write(MODULE_NAME, "[ERROR] runSimulation -> getElement -> datasetName = " + datasetName); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + } + } + } + catch (err) { + logger.write(MODULE_NAME, "[ERROR] runSimulation -> err = " + err.stack); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + } + } +}; + + +