From f8eb06b4866a8f72ad9d46b9e5a08fb3dd328fd1 Mon Sep 17 00:00:00 2001 From: Luis Fernando Molina Date: Fri, 5 Jun 2020 12:31:21 +0200 Subject: [PATCH] Overall Simulation Structure done --- Trading/Snapshots.js | 18 +- Trading/TradingSimulation.js | 391 +++++++++++++++++------------------ 2 files changed, 203 insertions(+), 206 deletions(-) diff --git a/Trading/Snapshots.js b/Trading/Snapshots.js index 31a7746b3..7edfd34f9 100644 --- a/Trading/Snapshots.js +++ b/Trading/Snapshots.js @@ -11,8 +11,17 @@ exports.newSnapshots = function newSnapshots(bot, logger) { let tradingEngine - /* We are going to take snapshots of the values of indicators at key moments of the sumulation. */ + /* Snapshots of Trigger On and Take Positions */ + let snapshots = { + headers: undefined, + triggerOn: [], + takePosition: [], + lastTriggerOn: undefined, + lastTakePosition: undefined + } + + /* We are going to take snapshots of the values of indicators at key moments of the sumulation. */ let snapshotKeys = new Map() snapshotLoopHeaders = [] snapshotDataRecord = [] @@ -68,10 +77,9 @@ exports.newSnapshots = function newSnapshots(bot, logger) { } - if (positionedAtYesterday === false) { - snapshots.triggerOn.push(closeValues.concat(snapshots.lastTriggerOn)) - snapshots.takePosition.push(closeValues.concat(snapshots.lastTakePosition)) - } + snapshots.triggerOn.push(closeValues.concat(snapshots.lastTriggerOn)) + snapshots.takePosition.push(closeValues.concat(snapshots.lastTakePosition)) + snapshots.lastTriggerOn = undefined snapshots.lastTakePosition = undefined addToSnapshots = false diff --git a/Trading/TradingSimulation.js b/Trading/TradingSimulation.js index 853945d00..4e24d8d2e 100644 --- a/Trading/TradingSimulation.js +++ b/Trading/TradingSimulation.js @@ -42,15 +42,6 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT processingDailyFiles = true } - /* Snapshots of Trigger On and Take Positions */ - let snapshots = { - headers: undefined, - triggerOn: [], - takePosition: [], - lastTriggerOn: undefined, - lastTakePosition: undefined - } - let tradingSystem = bot.TRADING_SYSTEM let tradingEngine = bot.TRADING_ENGINE let sessionParameters = bot.SESSION.parameters @@ -61,12 +52,10 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT let timerToCloseStage = 0 /* Stop Loss Management */ - const MIN_STOP_LOSS_VALUE = 0.0000000001 // 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 /* Take Profit Management */ - const MIN_TAKE_PROFIT_VALUE = 0.0000000001 // 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 @@ -80,6 +69,7 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT lastInstantOfTheDay = currentDay.valueOf() + ONE_DAY_IN_MILISECONDS - 1 } + /* These are the Modules we will need to run the Simulation */ const TRADING_ENGINE_MODULE = require('./TradingEngine.js') let tradingEngineModule = TRADING_ENGINE_MODULE.newTradingEngine(bot, logger) tradingEngineModule.initialize() @@ -104,17 +94,16 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT let tradingSystemModule = TRADING_SYSTEM_MODULE.newTradingSystem(bot, logger) tradingSystemModule.initialize() - /* Main Array and Maps */ + /* Setting up the candles array: The whole simulation is based on the array of candles at the time-frame defined at the session parameters. */ let propertyName = 'at' + sessionParameters.timeFrame.config.label.replace('-', '') let candles = chart[propertyName].candles - let currentChart = chart[propertyName] /* Last Candle */ - let lastCandle = candles[candles.length - 1] + let lastCandle = candles[candles.length - 1] // TODO Maybe remove this - /* For Loop Level heartbeat */ - let loopingDay - let previousLoopingDay + /* Variable needed for heartbeat functionality */ + let heartBeatDate + let previousheartBeatDate initializeLoop() @@ -162,191 +151,38 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT } 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 # ' + tradingEngine.current.candle.index.value) } - - let candle = candles[tradingEngine.current.candle.index.value] - - tradingEngineModule.setCurrentCandle(candle) - - /* Not processing while out of user-defined time range */ - if (candle.end < sessionParameters.timeRange.config.initialDatetime.valueOf()) { - if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Skipping Candle before the sessionParameters.timeRange.config.initialDatetime.') } - controlLoop() - return - } - if (candle.begin > sessionParameters.timeRange.config.finalDatetime.valueOf()) { - if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Skipping Candle after the sessionParameters.timeRange.config.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] - if (dataDependencyNode.referenceParent.config.codeName !== 'Multi-Period-Daily') { continue } - let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName - let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName - let currentElement = getElement(thisChart[pluralVariableName], candle, 'Daily' + '-' + mapKey + '-' + pluralVariableName) - thisChart[singularVariableName] = currentElement - } - } - } - - /* Finding the Current Element on Market Files */ - 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] - if (dataDependencyNode.referenceParent.config.codeName !== 'Multi-Period-Market') { continue } - let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName - let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName - let currentElement = getElement(thisChart[pluralVariableName], candle, 'Market' + '-' + mapKey + '-' + pluralVariableName) - thisChart[singularVariableName] = currentElement - } - } - - /* Finding the Current Element on Single Files */ - function isItInside(elementWithTimestamp, elementWithBeginEnd) { - if (elementWithTimestamp.timestamp >= elementWithBeginEnd.begin && elementWithTimestamp.timestamp <= elementWithBeginEnd.end) { - return true - } else { - return false - } - } - - let propertyName = 'atAnyTimeFrame' - let thisChart = chart[propertyName] - - for (let k = 0; k < dataDependencies.length; k++) { - let dataDependencyNode = dataDependencies[k] - if (dataDependencyNode.referenceParent.config.codeName !== 'Single-File') { continue } - let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName - let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName - let elementArray = thisChart[pluralVariableName] - let currentElement - if (elementArray !== undefined) { - currentElement = elementArray[elementArray.length - 1] - } - thisChart[singularVariableName] = currentElement - } - - /* While we are processing the previous day. */ - let positionedAtYesterday = false - if (currentDay) { - positionedAtYesterday = (candle.end < currentDay.valueOf()) - } + let candle = candles[tradingEngine.current.candle.index.value] // This is the current candle the Simulation is working at. 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()) } - let ticker = { - bid: candle.close, - ask: candle.close, - last: candle.close + if (checkIfWeAreAtTheHeadOfTheMarket() === false) { + afterLoop() + return } - /* 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 # ' + tradingEngine.current.candle.index.value + ' @ ' + processingDate) } - - /* Telling the world we are alive and doing well */ - let fromDate = new Date(sessionParameters.timeRange.config.initialDatetime.valueOf()) - let lastDate = new Date(sessionParameters.timeRange.config.finalDatetime.valueOf()) - - let currentDateString = loopingDay.getUTCFullYear() + '-' + utilities.pad(loopingDay.getUTCMonth() + 1, 2) + '-' + utilities.pad(loopingDay.getUTCDate(), 2) - let currentDate = new Date(loopingDay) - let percentage = global.getPercentage(fromDate, currentDate, lastDate) - bot.processHeartBeat(currentDateString, percentage) - - if (global.areEqualDates(currentDate, new Date()) === false) { - logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage) - } - } - previousLoopingDay = loopingDay.valueOf() - - tradingEngine.episode.episodeCounters.periods++ - tradingEngine.episode.episodeStatistics.days = tradingEngine.episode.episodeCounters.periods * sessionParameters.timeFrame.config.value / ONE_DAY_IN_MILISECONDS - - if (processingDailyFiles) { - /* We skip the candle at the head of the market because tradingEngine.current.candle.index.value has not closed yet. */ - let candlesPerDay = ONE_DAY_IN_MILISECONDS / sessionParameters.timeFrame.config.value - if (tradingEngine.current.candle.index.value === 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 tradingEngine.episode.episodeCounters.periods, then there will be a hole in the file since the last period will be missing. */ - } - } - } else { // We are processing Market Files - if (tradingEngine.current.candle.index.value === 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 - } + if (checkInitialAndFinalDatetime() === false) { + afterLoop() + return } + if (checkMinimunAndMaximunBalance() === false) { + afterLoop() + return + } + + + heartBeat() + + tradingEngineModule.setCurrentCandle(candle) // We move the current candle we are standing at, to the trading engine data structure to make it available to anyone, including conditions and formulas. + positionChartAtCurrentCandle() + + tradingEngineModule.updateEpisodeCountersAndStatistics() tradingSystemModule.setChart(chart) tradingSystemModule.evalConditions() tradingSystemModule.evalFormulas() - - - - /* Checks for Minimun and Maximun Balance. We do the check while not inside any strategy only. */ - if ( - tradingEngine.current.strategy.stageType.value === 'No Stage' && - tradingEngine.current.strategy.index.value === -1 - ) { - /* - First thing to do is to check if we are below the minimum balance, or above the maximun balance. - If we are, we are not gooing to trigger on again. - */ - let minimumBalance - let maximumBalance - let balance - - if (sessionParameters.sessionBaseAsset.name === bot.market.marketBaseAsset) { - balance = tradingEngine.current.balance.baseAsset.value - minimumBalance = sessionParameters.sessionBaseAsset.config.minimumBalance - maximumBalance = sessionParameters.sessionBaseAsset.config.maximumBalance - } else { - balance = tradingEngine.current.balance.quotedAsset.value - minimumBalance = sessionParameters.sessionQuotedAsset.config.minimumBalance - maximumBalance = sessionParameters.sessionQuotedAsset.config.maximumBalance - } - - let stopRunningDate = new Date(candle.begin) - if (balance <= minimumBalance) { - tradingSystem.error = 'Min Balance @ ' + stopRunningDate.toLocaleString() - if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> ' + tradingSystem.error) } - afterLoop() - return - } - - if (balance >= maximumBalance) { - tradingSystem.error = 'Max Balance @ ' + stopRunningDate.toLocaleString() - if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> ' + tradingSystem.error) } - afterLoop() - return - } - } - tradingSystemModule.checkTriggerOn() tradingSystemModule.checkTriggerOff() takePositionNow = tradingSystemModule.checkTakePosition() @@ -369,8 +205,7 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT tradingSystemModule.calculateTakeProfit() } - - tradingEngineModule.updatePositionCounters() + tradingEngineModule.updatePositionCountersAndStatistics() tradingEngineModule.updateDistanceToEventsCounters() /* Checking if Stop or Take Profit were hit */ @@ -381,8 +216,6 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT closePositionNow = tradingSystemModule.checkStopLossOrTakeProfitWasHit() } - - /* Taking a Position */ if ( takePositionNow === true @@ -403,7 +236,7 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT tradingEngineModule.closePosition() } - /* Closing the Closing Stage */ + /* Closing the Closing Stage TODO This should die once we have execution working. */ if (tradingEngine.current.strategy.stageType.value === 'Close Stage') { if (candle.begin - 5 * 60 * 1000 > timerToCloseStage) { tradingEngine.current.strategy.end.value = candle.end @@ -427,13 +260,7 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT tradingRecordsModule.addConditionsRecord() tradingRecordsModule.addStrategyRecord() tradingRecordsModule.addPositionRecord() - - /* We will remmember the last candle processed, so that if it is the last one of this run we can use it to continue from there next time. */ - tradingEngine.last.candle.begin = candle.begin - tradingEngine.last.candle.end = candle.end - - /* After everything at the simulation level was done, we will do the annoucements that are pending. */ - annoucementsModule.makeAnnoucements() + annoucementsModule.makeAnnoucements() // After everything at the simulation level was done, we will do the annoucements that are pending. controlLoop() } @@ -515,6 +342,168 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT callBackFunction(global.DEFAULT_FAIL_RESPONSE) } } + + function heartBeat() { + /* We will produce a simulation level heartbeat in order to inform the user this is running. */ + heartBeatDate = new Date(Math.trunc(candle.begin / ONE_DAY_IN_MILISECONDS) * ONE_DAY_IN_MILISECONDS) + if (heartBeatDate.valueOf() !== previousheartBeatDate) { + let processingDate = heartBeatDate.getUTCFullYear() + '-' + utilities.pad(heartBeatDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(heartBeatDate.getUTCDate(), 2) + + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Simulation ' + bot.sessionKey + ' Loop # ' + tradingEngine.current.candle.index.value + ' @ ' + processingDate) } + + /* Telling the world we are alive and doing well */ + let fromDate = new Date(sessionParameters.timeRange.config.initialDatetime.valueOf()) + let lastDate = new Date(sessionParameters.timeRange.config.finalDatetime.valueOf()) + + let currentDateString = heartBeatDate.getUTCFullYear() + '-' + utilities.pad(heartBeatDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(heartBeatDate.getUTCDate(), 2) + let currentDate = new Date(heartBeatDate) + let percentage = global.getPercentage(fromDate, currentDate, lastDate) + bot.processHeartBeat(currentDateString, percentage) + + if (global.areEqualDates(currentDate, new Date()) === false) { + logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage) + } + } + previousheartBeatDate = heartBeatDate.valueOf() + } + + function positionChartAtCurrentCandle() { + /* + In conditions and Formulas, we want users to have an easy sintax to refer to indicators. In order to achieve that, we need the user to have + easy access to the current candle for instance, or the current bollinger band, meaning the one the Simulation is currently standing at. + For that reason we do the following processing, to have at the chart data structure the current objects of each indicator / time frame. + */ + + /* Finding the Current Element on Market Files */ + 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] + if (dataDependencyNode.referenceParent.config.codeName !== 'Multi-Period-Daily') { continue } + let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName + let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName + let currentElement = getElement(thisChart[pluralVariableName], candle, 'Daily' + '-' + mapKey + '-' + pluralVariableName) + thisChart[singularVariableName] = currentElement + } + } + } + + /* Finding the Current Element on Market Files */ + 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] + if (dataDependencyNode.referenceParent.config.codeName !== 'Multi-Period-Market') { continue } + let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName + let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName + let currentElement = getElement(thisChart[pluralVariableName], candle, 'Market' + '-' + mapKey + '-' + pluralVariableName) + thisChart[singularVariableName] = currentElement + } + } + + /* Finding the Current Element At Single Files*/ + let propertyName = 'atAnyTimeFrame' + let thisChart = chart[propertyName] + + for (let k = 0; k < dataDependencies.length; k++) { + let dataDependencyNode = dataDependencies[k] + if (dataDependencyNode.referenceParent.config.codeName !== 'Single-File') { continue } + let singularVariableName = dataDependencyNode.referenceParent.parentNode.config.singularVariableName + let pluralVariableName = dataDependencyNode.referenceParent.parentNode.config.pluralVariableName + let elementArray = thisChart[pluralVariableName] + let currentElement + if (elementArray !== undefined) { + currentElement = elementArray[elementArray.length - 1] + } + thisChart[singularVariableName] = currentElement + } + } + + function checkIfWeAreAtTheHeadOfTheMarket() { + /* +We skip the candle at the head of the market because it has not closed yet. The procedure to determine if we are at the head of the market is different +when we are processing Market Files than when we are processing Daily Files. TODO Check that the procedure is good for Beta 6 +*/ + if (processingDailyFiles) { // We are processing Daily Files + let candlesPerDay = ONE_DAY_IN_MILISECONDS / sessionParameters.timeFrame.config.value + if (tradingEngine.current.candle.index.value === candles.length - 1) { + if ((candles.length < candlesPerDay) || (candles.length > candlesPerDay && candles.length < candlesPerDay * 2)) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkIfWeAreAtTheHeadOfTheMarket -> Skipping Candle because it is the last one and has not been closed yet.') } + return false + /* + 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 (tradingEngine.current.candle.index.value === candles.length - 1) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkIfWeAreAtTheHeadOfTheMarket -> Skipping Candle because it is the last one and has not been closed yet.') } + return false + } + } + return true + } + + function checkInitialAndFinalDatetime() { + /* Here we check that the current candle is not outside of user-defined time range at the session parameters.*/ + if (candle.end < sessionParameters.timeRange.config.initialDatetime.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle before the sessionParameters.timeRange.config.initialDatetime.') } + return false + } + if (candle.begin > sessionParameters.timeRange.config.finalDatetime.valueOf()) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle after the sessionParameters.timeRange.config.finalDatetime.') } + return false + } + return true + } + + function checkMinimunAndMaximunBalance() { + /* Checks for Minimun and Maximun Balance. We do the check while not inside any strategy only. */ + if ( + tradingEngine.current.strategy.stageType.value === 'No Stage' && + tradingEngine.current.strategy.index.value === -1 + ) { + /* + First thing to do is to check if we are below the minimum balance, or above the maximun balance. + If we are, we are not gooing to trigger on again. + */ + let minimumBalance + let maximumBalance + let balance + + if (sessionParameters.sessionBaseAsset.name === bot.market.marketBaseAsset) { + balance = tradingEngine.current.balance.baseAsset.value + minimumBalance = sessionParameters.sessionBaseAsset.config.minimumBalance + maximumBalance = sessionParameters.sessionBaseAsset.config.maximumBalance + } else { + balance = tradingEngine.current.balance.quotedAsset.value + minimumBalance = sessionParameters.sessionQuotedAsset.config.minimumBalance + maximumBalance = sessionParameters.sessionQuotedAsset.config.maximumBalance + } + + let stopRunningDate = new Date(candle.begin) + if (balance <= minimumBalance) { + tradingSystem.error = 'Min Balance @ ' + stopRunningDate.toLocaleString() + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkMinimunAndMaximunBalance -> ' + tradingSystem.error) } + return false + } + + if (balance >= maximumBalance) { + tradingSystem.error = 'Max Balance @ ' + stopRunningDate.toLocaleString() + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkMinimunAndMaximunBalance -> ' + tradingSystem.error) } + return false + } + } + return true + } } catch (err) { logger.write(MODULE_NAME, '[ERROR] runSimulation -> err = ' + err.stack) callBackFunction(global.DEFAULT_FAIL_RESPONSE)