diff --git a/Commons.js b/Commons.js index f3b4cc853..050791f24 100644 --- a/Commons.js +++ b/Commons.js @@ -75,7 +75,7 @@ return } if (outputDatasetNode.referenceParent.parentNode.dataBuilding === undefined) { - logger.write(MODULE_NAME, "[WARN] start -> Product Definition " + outputDatasetNode.referenceParent.parentNode.name + " without a Data Building Procedure. Product Definition = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode)); + logger.write(MODULE_NAME, "[WARN] start -> Product Definition " + outputDatasetNode.referenceParent.parentNode.name + " without a Data Building Procedure. Product Definition Name = " + outputDatasetNode.referenceParent.parentNode.name); } if (outputDatasetNode.referenceParent.config.codeName === undefined) { logger.write(MODULE_NAME, "[ERROR] start -> Dataset witn no codeName defined. Product Dataset = " + JSON.stringify(outputDatasetNode.referenceParent)); @@ -95,29 +95,43 @@ return } - if (outputDatasetNode.referenceParent.parentNode.parentNode === undefined) { - logger.write(MODULE_NAME, "[ERROR] start -> Product Definition not attached to a Bot. Product Definition = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode)); + let botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Indicator Bot') + if (botNode === undefined) { + botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Trading Bot') + } + if (botNode === undefined) { + logger.write(MODULE_NAME, "[ERROR] start -> Product Definition not attached to a Bot. Product Definition = "); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } - if (outputDatasetNode.referenceParent.parentNode.parentNode.config.codeName === undefined) { - logger.write(MODULE_NAME, "[ERROR] start -> Bot witn no codeName defined. Bot = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode.parentNode)); + if (botNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] start -> Bot witn no codeName defined."); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } - if (outputDatasetNode.referenceParent.parentNode.parentNode.parentNode === undefined) { - logger.write(MODULE_NAME, "[ERROR] start -> Bot not attached to a Data Mine. Bot = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode.parentNode)); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return - } - - if (outputDatasetNode.referenceParent.parentNode.parentNode.parentNode.config.codeName === undefined) { - logger.write(MODULE_NAME, "[ERROR] start -> Data Mine witn no codeName defined. Data Mine = " + JSON.stringify(outputDatasetNode.referenceParent.parentNode.parentNode.parentNode)); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return - } + let dataMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Data Mine') + if (dataMineNode === undefined) { + let tradingMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Trading Mine') + if (tradingMineNode === undefined) { + logger.write(MODULE_NAME, "[ERROR] start -> Bot not attached to a Data Mine or a Trading Mine."); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } else { + if (tradingMineNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] start -> Trading Mine witn no codeName defined."); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } + } + } else { + if (dataMineNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] start -> Data Mine witn no codeName defined."); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } + } } return true } diff --git a/DataDependencies.js b/DataDependencies.js index 918def0bf..54eb5916d 100644 --- a/DataDependencies.js +++ b/DataDependencies.js @@ -4,12 +4,12 @@ const LOG_FILE_CONTENT = false; const MODULE_NAME = "Data Dependencies"; - let bot = BOT + let bot = BOT let thisObject = { nodeArray: undefined, dataSetsModulesArray: [], - isItADepenency: isItADepenency, + isItADepenency: isItADepenency, initialize: initialize, finalize: finalize }; @@ -19,20 +19,22 @@ return thisObject; function initialize(callBackFunction) { - try { - - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } - /* Basic Valdidations */ if (bot.processNode.referenceParent.processDependencies !== undefined) { - if (bot.processNode.referenceParent.processDependencies.dataDependencies !== undefined) { - thisObject.nodeArray = bot.processNode.referenceParent.processDependencies.dataDependencies - } else { + thisObject.nodeArray = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency') + if (thisObject.nodeArray.length === 0) { logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> It is not possible to not have data dependencies at all."); callBackFunction(global.DEFAULT_OK_RESPONSE) return } + /* + We will filter ourt declared dependencies that are not present in the workspace. + This will allow the user to have less Data Mines loaded at the workspace + that the ones that a Trading Mine depends on. + */ + thisObject.nodeArray = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(thisObject.nodeArray) + } else { logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> It is not possible to not have process dependencies, which means not data dependencies."); callBackFunction(global.DEFAULT_OK_RESPONSE) @@ -51,46 +53,62 @@ if (bot.DEPENDENCY_FILTER !== undefined) { for (let i = 0; i < bot.DEPENDENCY_FILTER.length; i++) { let key = bot.DEPENDENCY_FILTER[i] - filter.set(key, true) + filter.set(key, true) } } /* For each dependency declared at the nodeArray, we will initialize a DataSet as part of this initialization process. */ - let alreadyCalledBack = false; - let addCount = 0; + let alreadyCalledBack = false + let addCount = 0 + let skipCount = 0 + + /* + The current nodeArray that we have includes all the dependencies daclared in the Data Mine / Trading Mine + process dependencies, minus the ones without reference parent, meaning that references Data Mines that are + not present at the workspace. From all the remaining dependencies there will be others that we need to + filter out, and they are the ones which the user does not have a data product anywhere on the network where + this task is running, that is storing the data associated to this dependency. The way we use to filter this + out is by analizyng the response of the initialization call to the DataSet module. If it did not fail but + it was not initialized, then we will assume this happened because the data could not be located and we will + assume the user did this on purpose and we will filter out this dependency by not putting it on this new array. + */ + let newNodeArray = [] for (let i = 0; i < thisObject.nodeArray.length; i++) { - let dataSetModule = DATA_SET.newDataSet(BOT, logger); - dataSetModule.initialize(thisObject.nodeArray[i], onInitilized); - function onInitilized(err) { + function onInitilized(err, wasInitialized) { if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> err = "+ JSON.stringify(err)); + logger.write(MODULE_NAME, "[ERROR] initialize -> onInitilized -> err = " + JSON.stringify(err)); alreadyCalledBack = true; callBackFunction(err); return; } - addDataSet(); + if (wasInitialized === true) { + addCount++; + newNodeArray.push(thisObject.nodeArray[i]) + addDataSet() + } else { + skipCount++ + } + checkIfWeAreDone() } - function addDataSet() { - - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> addDataSet -> Entering function."); } - - addCount++; - + function addDataSet() { thisObject.dataSetsModulesArray.push(dataSetModule); + } - if (addCount === thisObject.nodeArray.length) { + function checkIfWeAreDone() { + if (addCount + skipCount === thisObject.nodeArray.length) { if (alreadyCalledBack === false) { alreadyCalledBack = true + thisObject.nodeArray = newNodeArray callBackFunction(global.DEFAULT_OK_RESPONSE); return; } @@ -99,14 +117,14 @@ } } catch (err) { - logger.write(MODULE_NAME, "[ERROR] initialize -> err = "+ err.stack); + logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack); callBackFunction(global.DEFAULT_FAIL_RESPONSE); } } function finalize() { for (let i = 0; i < thisObject.dataSetsModulesArray.length; i++) { - let dataSetModule = thisObject.dataSetsModulesArray[i] + let dataSetModule = thisObject.dataSetsModulesArray[i] dataSetModule.finalize() } thisObject.dataSetsModulesArray = undefined @@ -115,7 +133,7 @@ } function isItADepenency(timeFrame, product) { - let key = timeFrame + '-' + product + let key = timeFrame + '-' + product return filter.get(key) } diff --git a/DataSet.js b/DataSet.js index bb7f789e3..5b43b7bbf 100644 --- a/DataSet.js +++ b/DataSet.js @@ -23,9 +23,6 @@ exports.newDataSet = function newDataSet(BOT, logger) { function initialize(dataDependency, callBackFunction) { try { - - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } - thisObject.node = dataDependency.referenceParent; logger.fileName = MODULE_NAME + "." + thisObject.node.type + "." + thisObject.node.name + "." + thisObject.node.id; @@ -67,7 +64,7 @@ exports.newDataSet = function newDataSet(BOT, logger) { } if (thisObject.node.parentNode.parentNode.parentNode === undefined) { - logger.write(MODULE_NAME, "[ERROR] initialize -> Bot not attached to a Data Mine. Bot = " + JSON.stringify(thisObject.node.parentNode.parentNode)); + logger.write(MODULE_NAME, "[ERROR] initialize -> Bot not attached to a Data Mine or Trading Mine. Bot = " + JSON.stringify(thisObject.node.parentNode.parentNode)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } @@ -82,77 +79,22 @@ exports.newDataSet = function newDataSet(BOT, logger) { let network = global.TASK_NETWORK let datasetProductDefinition = thisObject.node.parentNode - for (let i = 0; i < network.networkNodes.length; i++) { - let networkNode = network.networkNodes[i] - if (networkNode.dataStorage !== undefined) { - if (networkNode.dataStorage.sessionIndependentData !== undefined) { - for (let j = 0; j < networkNode.dataStorage.sessionIndependentData.exchangeDataProducts.length; j++) { - let exchangeDataProduct = networkNode.dataStorage.sessionIndependentData.exchangeDataProducts[j] - for (let k = 0; k < exchangeDataProduct.singleMarketData.length; k++) { - let singleMarketData = exchangeDataProduct.singleMarketData[k] - if (singleMarketData.referenceParent !== undefined) { - let market = singleMarketData.referenceParent - let currentProcessMarket = bot.processNode.marketReference.referenceParent + let nodeArray = global.NODE_MESH_TO_PATH_ARRAY(network, datasetProductDefinition.id) + let networkNode = global.FIND_NODE_IN_NODE_ARRAY(nodeArray, 'Network Node') - if (currentProcessMarket.id === market.id) { - for (let m = 0; m < singleMarketData.dataProducts.length; m++) { - let dataProduct = singleMarketData.dataProducts[m] - if (dataProduct.referenceParent !== undefined) { - let productDefinition = dataProduct.referenceParent - if (datasetProductDefinition.id === productDefinition.id) { - - /* We found where the data is located on the network. */ - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving data from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); } - - fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort); - callBackFunction(global.DEFAULT_OK_RESPONSE); - return - } - } - } - } - } - } - } - } - if (networkNode.dataStorage.sessionBasedData !== undefined) { - for (let j = 0; j < networkNode.dataStorage.sessionBasedData.exchangeDataProducts.length; j++) { - let exchangeDataProduct = networkNode.dataStorage.sessionBasedData.exchangeDataProducts[j] - for (let s = 0; s < exchangeDataProduct.sessionReferences.length; s++) { - let sessionReference = exchangeDataProduct.sessionReferences[s] - let singleMarketData = sessionReference.singleMarketData - if (singleMarketData.referenceParent !== undefined) { - let market = singleMarketData.referenceParent - let currentProcessMarket = bot.processNode.marketReference.referenceParent - - if (currentProcessMarket.id === market.id) { - for (let m = 0; m < singleMarketData.dataProducts.length; m++) { - let dataProduct = singleMarketData.dataProducts[m] - if (dataProduct.referenceParent !== undefined) { - let productDefinition = dataProduct.referenceParent - if (datasetProductDefinition.id === productDefinition.id) { - - /* We found where the data is located on the network. */ - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving data from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); } - - fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort); - callBackFunction(global.DEFAULT_OK_RESPONSE); - return - } - } - } - } - } - } - } - } - } + if (networkNode === undefined) { + logger.write(MODULE_NAME, "[WARN] initialize -> Network Node not found.") + logger.write(MODULE_NAME, "[WARN] initialize -> Initialization Failed because we could not find where the data of this dataset is located within the network. Check the logs for more info."); + logger.write(MODULE_NAME, "[WARN] initialize -> Could not find where " + datasetProductDefinition.name + " for " + bot.exchange + " " + bot.market.baseAsset + "/" + bot.market.quotedAsset + " is stored within the network."); + callBackFunction(global.DEFAULT_OK_RESPONSE, false); + return } - logger.write(MODULE_NAME, "[ERROR] initialize -> Initialization Failed because we could not find where the data of this dataset is located within the network. Check the logs for more info."); - logger.write(MODULE_NAME, "[ERROR] initialize -> Could not find where " + datasetProductDefinition.name + " for " + bot.exchange + " " + bot.market.baseAsset + "/" + bot.market.quotedAsset + " is stored within the network."); + /* We found where the data is located on the network. */ + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving data from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); } - callBackFunction(global.DEFAULT_FAIL_RESPONSE); + fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort); + callBackFunction(global.DEFAULT_OK_RESPONSE, true); } catch (err) { logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack); diff --git a/DebugLog.js b/DebugLog.js index e7c874e9b..a1ef69a37 100644 --- a/DebugLog.js +++ b/DebugLog.js @@ -101,7 +101,7 @@ exports.newDebugLog = function newDebugLog() { writeLog(); /* This is the implementation of the mechanism to auto-mantain logs. */ - if (contentToPersist.indexOf('[ERROR]') < 0 && contentToPersist.indexOf('[PERSIST]') < 0) { + if (contentToPersist.indexOf('[ERROR]') < 0 && contentToPersist.indexOf('[PERSIST]') < 0 && contentToPersist.indexOf('[IMPORTANT]') < 0) { thisObject.bot.LOGS_TO_DELETE_QUEUE.push(filePath + '/' + fileName) } if (thisObject.bot.LOGS_TO_DELETE_QUEUE.length > thisObject.bot.DELETE_QUEUE_SIZE) { diff --git a/FileStorage.js b/FileStorage.js index 1b55b9af7..8cc7baac9 100644 --- a/FileStorage.js +++ b/FileStorage.js @@ -4,7 +4,7 @@ const path = require('path') exports.newFileStorage = function newFileStorage(logger, host, port) { const MODULE_NAME = 'FileStorage' - const MAX_RETRY = 30 + const MAX_RETRY = 10 const FAST_RETRY_TIME_IN_MILISECONDS = 500 const SLOW_RETRY_TIME_IN_MILISECONDS = 2000 @@ -17,6 +17,8 @@ exports.newFileStorage = function newFileStorage(logger, host, port) { } let thisObject = { + asyncGetTextFile: asyncGetTextFile, + asyncCreateTextFile: asyncCreateTextFile, getTextFile: getTextFile, createTextFile: createTextFile, deleteTextFile: deleteTextFile @@ -24,6 +26,41 @@ exports.newFileStorage = function newFileStorage(logger, host, port) { return thisObject + async function asyncGetTextFile(filePath, noRetry, canUsePrevious) { + /* This function allows its caller to work with async / await instead of callbacks */ + let promise = new Promise((resolve, reject) => { + + getTextFile(filePath, onFileRead, noRetry, canUsePrevious) + function onFileRead(err, text) { + + let response = { + err: err, + text: text + } + resolve(response) + } + }) + + return promise + } + + async function asyncCreateTextFile(filePath, fileContent, keepPrevious, noTemp) { + /* This function allows its caller to work with async / await instead of callbacks */ + let promise = new Promise((resolve, reject) => { + + createTextFile(filePath, fileContent, onFileWriten, keepPrevious, noTemp) + function onFileWriten(err) { + + let response = { + err: err + } + resolve(response) + } + }) + + return promise + } + function getTextFile(filePath, callBackFunction, noRetry, canUsePrevious) { let currentRetryGetTextFile = 0 @@ -31,9 +68,6 @@ exports.newFileStorage = function newFileStorage(logger, host, port) { recursiveGetTextFile(filePath, callBackFunction, noRetry, canUsePrevious) function recursiveGetTextFile(filePath, callBackFunction, noRetry, canUsePrevious) { - - logger.write(MODULE_NAME, '[INFO] FileStorage -> getTextFile -> Entering Function.') - let fileDoesNotExist = false /* Choose path for either bots or data */ @@ -163,7 +197,7 @@ exports.newFileStorage = function newFileStorage(logger, host, port) { } function retry() { - //console.log(new Date(), ' Retry #: ' + currentRetryGetTextFile, fileLocation ) + console.log(new Date(), '[WARN] Read File Retry #: ' + currentRetryGetTextFile, fileLocation) if (currentRetryGetTextFile < MAX_RETRY) { currentRetryGetTextFile++ logger.write(MODULE_NAME, '[WARN] FileStorage -> getTextFile -> retry -> Will try to read the file again -> Retry #: ' + currentRetryGetTextFile) @@ -219,12 +253,12 @@ exports.newFileStorage = function newFileStorage(logger, host, port) { */ const fs = require('fs') if (noTemp === true) { - fs.writeFile(fileLocation, fileContent, onFileWritenn) + fs.writeFile(fileLocation, fileContent, onFileWriten) } else { - fs.writeFile(fileLocation + '.tmp', fileContent, onFileWritenn) + fs.writeFile(fileLocation + '.tmp', fileContent, onFileWriten) } - function onFileWritenn(err) { + function onFileWriten(err) { let retryTimeToUse = FAST_RETRY_TIME_IN_MILISECONDS if (currentRetryWriteTextFile > MAX_RETRY - 2) { retryTimeToUse = SLOW_RETRY_TIME_IN_MILISECONDS diff --git a/IndicatorBot.js b/IndicatorBot.js index dbe4b844f..cd513dee4 100644 --- a/IndicatorBot.js +++ b/IndicatorBot.js @@ -36,7 +36,7 @@ } /* This bot is not ready for taking its code from the UI, then we need to load it from its repo. */ - let filePath = bot.dataMine + "/" + "bots" + "/" + bot.repo + "/" + pProcessConfig.codeName + let filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo + "/" + pProcessConfig.codeName filePath += "/User.Bot.js" fileStorage.getTextFile(filePath, onBotDownloaded); @@ -54,7 +54,7 @@ USER_BOT_MODULE = {} USER_BOT_MODULE.newUserBot = eval(text); // TODO This needs to be changed function - filePath = bot.dataMine + "/" + "bots" + "/" + bot.repo; + filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo filePath += "/Commons.js" fileStorage.getTextFile(filePath, onCommonsDownloaded); @@ -81,9 +81,6 @@ try { let fixedTimeLoopIntervalHandle; - /* Errors sent to the UI */ - bot.processError = processError - /* Heartbeats sent to the UI */ bot.processHeartBeat = processHeartBeat @@ -145,7 +142,7 @@ function initializeProcessExecutionEvents() { try { processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger) - processExecutionEvents.initialize(onInizialized); + processExecutionEvents.initialize(processConfig, onInizialized); function onInizialized(err) { try { @@ -763,26 +760,9 @@ global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event) } - function processError(node, errorMessage) { - let event - if (node !== undefined) { - event = { - nodeName: node.name, - nodeType: node.type, - nodeId: node.id, - errorMessage: errorMessage - } - } else { - event = { - errorMessage: errorMessage - } - } - global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Error', event) - } - function processStopped() { if (global.unexpectedError !== undefined) { - processError(undefined, "An unexpected error caused the Process to stop.") + global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.") } else { global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped') } diff --git a/IndicatorOutput.js b/IndicatorOutput.js index 3faa4052e..d7f258b6b 100644 --- a/IndicatorOutput.js +++ b/IndicatorOutput.js @@ -24,14 +24,10 @@ } function initialize(callBackFunction) { - try { - logger.fileName = MODULE_NAME; logger.initialize(); - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } - callBackFunction(global.DEFAULT_OK_RESPONSE); } catch (err) { @@ -57,10 +53,11 @@ } /* The first phase here is about checking that we have everything we need at the definition level. */ - let dataDependencies = bot.processNode.referenceParent.processDependencies.dataDependencies + let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency') + if (commons.validateDataDependencies(dataDependencies, callBackFunction) !== true) { return } - let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets + let outputDatasets = global.NODE_BRANCH_TO_ARRAY (bot.processNode.referenceParent.processOutput, 'Output Dataset') if (commons.validateOutputDatasets(outputDatasets, callBackFunction) !== true) { return } /* The second phase is about transforming the inputs into a format that can be used to apply the user defined code. */ @@ -134,8 +131,11 @@ contextSummary.dataset = outputDatasetNode.referenceParent.config.codeName contextSummary.product = outputDatasetNode.referenceParent.parentNode.config.codeName - contextSummary.bot = outputDatasetNode.referenceParent.parentNode.parentNode.config.codeName - contextSummary.dataMine = outputDatasetNode.referenceParent.parentNode.parentNode.parentNode.config.codeName + + let botNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Indicator Bot') + contextSummary.bot = botNode.config.codeName + let dataMineNode = global.FIND_NODE_IN_NODE_MESH(outputDatasetNode, 'Data Mine') + contextSummary.dataMine = dataMineNode.config.codeName let fileContent = commons.generateFileContent(outputData, outputDatasetNode.referenceParent.parentNode.record, resultsWithIrregularPeriods, processingDailyFiles, currentDay, callBackFunction) commons.writeFile(contextSummary, fileContent, anotherFileWritten, processingDailyFiles, timeFrameLabel, currentDay, callBackFunction) diff --git a/LowFrequencyTrading/Announcements.js b/LowFrequencyTrading/Announcements.js index ec8c25d75..2c63c88d1 100644 --- a/LowFrequencyTrading/Announcements.js +++ b/LowFrequencyTrading/Announcements.js @@ -41,11 +41,7 @@ exports.newAnnouncements = function newAnnouncements(bot, logger) { if (canAnnounce === true) { let text = announcement.config.text if (announcement.formula !== undefined) { - try { - text = eval(announcement.formula.code) - } catch (err) { - tradingSystem.errors.push([announcement.formula.id, err.message]) - } + text = tradingSystem.formulas.get(announcement.formula.id) } if (bot.SESSION.socialBots !== undefined) { diff --git a/LowFrequencyTrading/ExchangeAPI.js b/LowFrequencyTrading/ExchangeAPI.js index 633f9a59c..a6d314c43 100644 --- a/LowFrequencyTrading/ExchangeAPI.js +++ b/LowFrequencyTrading/ExchangeAPI.js @@ -35,6 +35,7 @@ 'timeout': 30000, 'enableRateLimit': true, verbose: false, + 'adjustForTimeDifference': true, options: options } exchange = new exchangeClass(exchangeConstructorParams) @@ -50,11 +51,11 @@ async function getOrder(tradingSystemOrder, tradingEngineOrder) { let orderId = tradingEngineOrder.exchangeId.value + const symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset /* Basic Logging */ - logInfo("getOrder -> Entering function. orderId = " + orderId); - - const symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset + logInfo("getOrder -> orderId = " + orderId) + logInfo("getOrder -> symbol = " + symbol) /* Basic Validations */ if (exchange.has['fetchOrder'] === false) { @@ -64,9 +65,11 @@ try { let order = await exchange.fetchOrder(orderId, symbol) + + logInfo("getOrder -> Response from the Exchange: " + JSON.stringify(order)); return order } catch (err) { - tradingSystem.errors.push([tradingSystemOrder.id, err.message]) + tradingSystem.errors.push([tradingSystemOrder.id, "getOrder -> Error = " + err.message]) logError("getOrder -> Error = " + err.message); } } @@ -74,10 +77,10 @@ async function createOrder(tradingSystemOrder, tradingEngineOrder) { let price = tradingEngineOrder.rate.value // CCXT: how much quote currency you are willing to pay for a trade lot of base currency (for limit orders only) - let amount = tradingEngineOrder.size.value // CCXT: how much of currency you want to trade let type // CCXT: a string literal type of order, ccxt currently unifies market and limit orders only let side // CCXT: a string literal for the direction of your order, buy or sell let symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset // CCXT: a string literal symbol of the market you wish to trade on, like BTC/USD, ZEC/ETH, DOGE/DASH, etc + let amount = tradingEngineOrder.orderBaseAsset.size.value // CCXT: how much of currency you want to trade, in Base Asset. switch (tradingSystemOrder.type) { case 'Market Buy Order': { @@ -121,10 +124,12 @@ try { let order = await (exchange.createOrder(symbol, type, side, amount, price)) + + logInfo("createOrder -> Response from the Exchange: " + JSON.stringify(order)); return order.id } catch (err) { - tradingSystem.errors.push([tradingSystemOrder.id, err.message]) - logError("getOrder -> Error = " + err.message); + tradingSystem.errors.push([tradingSystemOrder.id, "createOrder -> Error = " + err.message]) + logError("createOrder -> Error = " + err.message); } } @@ -133,22 +138,24 @@ let orderId = tradingEngineOrder.exchangeId.value /* Basic Logging */ - logInfo("getOrder -> Entering function. orderId = " + orderId); + logInfo("cancelOrder -> Entering function. orderId = " + orderId); const symbol = bot.market.baseAsset + '/' + bot.market.quotedAsset /* Basic Validations */ if (exchange.has['fetchOrder'] === false) { - logError("getOrder -> Exchange does not support fetchOrder command."); + logError("cancelOrder -> Exchange does not support fetchOrder command."); return } try { let order = await exchange.cancelOrder(orderId, symbol) + + logInfo("cancelOrder -> Response from the Exchange: " + JSON.stringify(order)); return true } catch (err) { - tradingSystem.errors.push([tradingSystemOrder.id, err.message]) - logError("getOrder -> Error = " + err.message); + tradingSystem.errors.push([tradingSystemOrder.id, "cancelOrder -> Error = " + err.message]) + logError("cancelOrder -> Error = " + err.message); } } diff --git a/LowFrequencyTrading/Snapshots.js b/LowFrequencyTrading/Snapshots.js index 89c10767b..6f1b02c5a 100644 --- a/LowFrequencyTrading/Snapshots.js +++ b/LowFrequencyTrading/Snapshots.js @@ -5,7 +5,7 @@ exports.newSnapshots = function newSnapshots(bot, logger) { const MODULE_NAME = 'Snapshots' let thisObject = { - reset: reset, + updateChart: updateChart, strategyEntry: strategyEntry, strategyExit: strategyExit, positionEntry: positionEntry, @@ -60,9 +60,8 @@ exports.newSnapshots = function newSnapshots(bot, logger) { chart = undefined } - function reset(pChart) { - /* This function helps reset data structures at every cycle of the simulation loop/=. */ - chart = pChart // We need chat to be a local object accessible from conditions and formulas. + function updateChart(pChart) { + chart = pChart } function strategyEntry() { @@ -125,7 +124,7 @@ exports.newSnapshots = function newSnapshots(bot, logger) { } /* Now we go down through all this node children */ - let nodeDefinition = bot.APP_SCHEMA_MAP.get(node.type) + let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type) if (nodeDefinition === undefined) { return } if (nodeDefinition.properties !== undefined) { @@ -237,8 +236,13 @@ exports.newSnapshots = function newSnapshots(bot, logger) { snapshots.headers.push(key) } - let value = eval(instruction) - values.push(value) + try { + let value = eval(instruction) + values.push(value) + } catch (err) { + tradingSystem.errors.push([nodeWithCode.id, 'Instruction ' + instruction + ' evalueates with error ' + err.message]) + values.push(0) + } } } } @@ -252,16 +256,18 @@ exports.newSnapshots = function newSnapshots(bot, logger) { function getResults(openDatetime, closeDatetime) { - let closeHeaders = ['Trade Number', 'Open Datetime', 'Close Datetime', 'Strategy Name', 'Trigger On Situation', 'Take Position Situation', 'Result', 'ROI', 'Exit Type'] + let closeHeaders = ['Trade Number', 'Open Datetime', 'Close Datetime', 'Strategy Name', 'Trigger On Situation', 'Take Position Situation', 'Result In Base Asset', 'Result In Quoted Asset', 'ROI in Base Asset', 'ROI in Quoted Asset', 'Exit Type'] closeValues = [ - tradingEngine.episode.episodeCounters.positions.value, // Position Number + tradingEngine.current.episode.episodeCounters.positions.value, // Position Number (new Date(openDatetime)).toISOString(), // Open Datetime (new Date(closeDatetime)).toISOString(), // Open Datetime tradingEngine.current.strategy.strategyName.value, // Strategy Name tradingEngine.current.strategy.situationName.value, // Trigger On Situation tradingEngine.current.position.situationName.value, // Take Position Situation - tradingEngine.current.position.positionStatistics.hitFail.value, // Result - tradingEngine.current.position.positionStatistics.ROI.value, // ROI + tradingEngine.current.position.positionBaseAsset.hitFail.value, // Result in Base Asset + tradingEngine.current.position.positionQuotedAsset.hitFail.value, // Result in Base Asset + tradingEngine.current.position.positionBaseAsset.ROI.value, // ROI in Base Asseet + tradingEngine.current.position.positionQuotedAsset.ROI.value, // ROI in Quoted Asset tradingEngine.current.position.exitType.value // Exit Type ] diff --git a/LowFrequencyTrading/TradingEngine.js b/LowFrequencyTrading/TradingEngine.js index e10bfe927..b9dc6f889 100644 --- a/LowFrequencyTrading/TradingEngine.js +++ b/LowFrequencyTrading/TradingEngine.js @@ -5,11 +5,13 @@ exports.newTradingEngine = function newTradingEngine(bot, logger) { */ const MODULE_NAME = 'Trading Engine' let thisObject = { + mantain: mantain, + reset: reset, getNodeById: getNodeById, cloneValues: cloneValues, setCurrentCandle: setCurrentCandle, - updateEpisodeCountersAndStatistics: updateEpisodeCountersAndStatistics, - updateDistanceToEventsCounters: updateDistanceToEventsCounters, + setCurrentCycle: setCurrentCycle, + initializeNode: initializeNode, initialize: initialize, finalize: finalize } @@ -21,15 +23,18 @@ exports.newTradingEngine = function newTradingEngine(bot, logger) { return thisObject function initialize() { - /* Here we will go through all the nodes in the Trading Engine hiriarchy and apply the initial value to the value property when needed */ tradingEngine = bot.simulationState.tradingEngine sessionParameters = bot.SESSION.parameters + initializeNodeMap(tradingEngine) + if (bot.FIRST_EXECUTION === true) { + /* + Here we will go through all the nodes in the Trading Engine hiriarchy and + apply the initial value to the value property when needed + */ initializeNode(tradingEngine) - tradingEngine.current.balance.baseAsset.value = sessionParameters.sessionBaseAsset.config.initialBalance - tradingEngine.current.balance.quotedAsset.value = sessionParameters.sessionQuotedAsset.config.initialBalance } } @@ -39,19 +44,51 @@ exports.newTradingEngine = function newTradingEngine(bot, logger) { nodesMap = undefined } + function mantain() { + } + + function reset() { + + } + function getNodeById(NodeId) { return nodesMap.get(NodeId) } function setCurrentCandle(candle) { - cloneValues(tradingEngine.current.candle, tradingEngine.previous.candle) + tradingEngine.current.episode.candle.begin.value = candle.begin + tradingEngine.current.episode.candle.end.value = candle.end + tradingEngine.current.episode.candle.open.value = candle.open + tradingEngine.current.episode.candle.close.value = candle.close + tradingEngine.current.episode.candle.min.value = candle.min + tradingEngine.current.episode.candle.max.value = candle.max + } - tradingEngine.current.candle.begin.value = candle.begin - tradingEngine.current.candle.end.value = candle.end - tradingEngine.current.candle.open.value = candle.open - tradingEngine.current.candle.close.value = candle.close - tradingEngine.current.candle.min.value = candle.min - tradingEngine.current.candle.max.value = candle.max + function setCurrentCycle(cycle) { + tradingEngine.current.episode.cycle.value = cycle + switch (cycle) { + case 'First': { + tradingEngine.current.episode.cycle.begin.value = + tradingEngine.current.episode.candle.begin.value + + sessionParameters.timeFrame.config.value + tradingEngine.current.episode.cycle.end.value = + tradingEngine.current.episode.candle.begin.value + + sessionParameters.timeFrame.config.value + + sessionParameters.timeFrame.config.value / 2 - 1 + break + } + case 'Second': { + tradingEngine.current.episode.cycle.begin.value = + tradingEngine.current.episode.candle.begin.value + + sessionParameters.timeFrame.config.value + + sessionParameters.timeFrame.config.value / 2 + tradingEngine.current.episode.cycle.end.value = + tradingEngine.current.episode.candle.begin.value + + sessionParameters.timeFrame.config.value + + sessionParameters.timeFrame.config.value * 4 / 4 - 1 + break + } + } } function cloneValues(originNode, destinationNode) { @@ -62,7 +99,7 @@ exports.newTradingEngine = function newTradingEngine(bot, logger) { destinationNode.value = originNode.value /* Now we go down through all the children of the origin node, assuming the destination node has the same children structure*/ - let nodeDefinition = bot.APP_SCHEMA_MAP.get(originNode.type) + let nodeDefinition = global.APP_SCHEMA_MAP.get(originNode.type) if (nodeDefinition === undefined) { return } if (nodeDefinition.properties !== undefined) { @@ -95,21 +132,56 @@ exports.newTradingEngine = function newTradingEngine(bot, logger) { } } - function initializeNode(node) { + function initializeNodeMap(node) { if (node === undefined) { return } /* Add to Node Map */ nodesMap.set(node.id, node) + /* Now we go down through all this node children */ + let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type) + if (nodeDefinition === undefined) { return } + + if (nodeDefinition.properties !== undefined) { + let previousPropertyName // Since there are cases where there are many properties with the same name,because they can hold nodes of different types but only one at the time, we have to avoind counting each property of those as individual children. + for (let i = 0; i < nodeDefinition.properties.length; i++) { + let property = nodeDefinition.properties[i] + + switch (property.type) { + case 'node': { + if (property.name !== previousPropertyName) { + if (node[property.name] !== undefined) { + initializeNodeMap(node[property.name]) + } + previousPropertyName = property.name + } + break + } + case 'array': { + if (node[property.name] !== undefined) { + let nodePropertyArray = node[property.name] + for (let m = 0; m < nodePropertyArray.length; m++) { + initializeNodeMap(nodePropertyArray[m]) + } + } + break + } + } + } + } + } + + function initializeNode(node) { + if (node === undefined) { return } + /* Here we initialize the node value */ if (node.config !== undefined) { if (node.config.initialValue !== undefined) { node.value = node.config.initialValue } } - node.initialize = initializeNode // This will allow anyone to initialize this node and its children. /* Now we go down through all this node children */ - let nodeDefinition = bot.APP_SCHEMA_MAP.get(node.type) + let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type) if (nodeDefinition === undefined) { return } if (nodeDefinition.properties !== undefined) { @@ -140,68 +212,5 @@ exports.newTradingEngine = function newTradingEngine(bot, logger) { } } } - - function updateEpisodeCountersAndStatistics() { - tradingEngine.episode.episodeCounters.periods.value++ - tradingEngine.episode.episodeStatistics.days.value = tradingEngine.episode.episodeCounters.periods.value * sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS - } - - function updateDistanceToEventsCounters() { - /* Keeping Distance Counters Up-to-date while avoinding counting before the first event happens. */ - if ( - tradingEngine.current.distanceToEvent.triggerOn.value > 0 - ) { - tradingEngine.current.distanceToEvent.triggerOn.value++ - } - - if ( - tradingEngine.current.distanceToEvent.triggerOff.value > 0 - ) { - tradingEngine.current.distanceToEvent.triggerOff.value++ - } - - if ( - tradingEngine.current.distanceToEvent.takePosition.value > 0 - ) { - tradingEngine.current.distanceToEvent.takePosition.value++ - } - - if ( - tradingEngine.current.distanceToEvent.closePosition.value > 0 - ) { - tradingEngine.current.distanceToEvent.closePosition.value++ - } - - if ( - tradingEngine.current.distanceToEvent.nextPhase.value > 0 - ) { - tradingEngine.current.distanceToEvent.nextPhase.value++ - } - - if ( - tradingEngine.current.distanceToEvent.moveToPhase.value > 0 - ) { - tradingEngine.current.distanceToEvent.moveToPhase.value++ - } - - if ( - tradingEngine.current.distanceToEvent.createOrder.value > 0 - ) { - tradingEngine.current.distanceToEvent.createOrder.value++ - } - - if ( - tradingEngine.current.distanceToEvent.cancelOrder.value > 0 - ) { - tradingEngine.current.distanceToEvent.cancelOrder.value++ - } - - if ( - tradingEngine.current.distanceToEvent.closeOrder.value > 0 - ) { - tradingEngine.current.distanceToEvent.closeOrder.value++ - } - } - } diff --git a/LowFrequencyTrading/TradingEpisode.js b/LowFrequencyTrading/TradingEpisode.js new file mode 100644 index 000000000..76bb7a8d3 --- /dev/null +++ b/LowFrequencyTrading/TradingEpisode.js @@ -0,0 +1,317 @@ +exports.newTradingEpisode = function newTradingEpisode(bot, logger, tradingEngineModule) { + /* + This module packages all functions related to Episodes. + */ + const MODULE_NAME = 'Trading Episode' + let thisObject = { + mantain: mantain, + reset: reset, + openEpisode: openEpisode, + updateExitType: updateExitType, + closeEpisode: closeEpisode, + cycleBasedStatistics: cycleBasedStatistics, + initialize: initialize, + finalize: finalize + } + + let tradingEngine + let tradingSystem + let sessionParameters + + return thisObject + + function initialize() { + tradingSystem = bot.simulationState.tradingSystem + tradingEngine = bot.simulationState.tradingEngine + sessionParameters = bot.SESSION.parameters + } + + function finalize() { + tradingEngine = undefined + tradingSystem = undefined + sessionParameters = undefined + } + + function mantain() { + updateCounters() + updateStatistics() + updateEnds() + updateDistanceToEventsCounters() + } + + function reset() { + resetTradingEngineDataStructure() + } + + function openEpisode() { + /* + This function is called each time the simulation starts. That does not mean the Episode + must be opened there, since it might happen that it is looping at the end of the market + or the task / session was restarted. + */ + if (bot.FIRST_EXECUTION === true) { + /* Getting the begin Balance from the session configuration */ + tradingEngine.current.episode.episodeBaseAsset.beginBalance.value = sessionParameters.sessionBaseAsset.config.initialBalance + tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value = sessionParameters.sessionQuotedAsset.config.initialBalance + + /* The current balance is also the begin balance, that is how this starts. */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = sessionParameters.sessionBaseAsset.config.initialBalance + tradingEngine.current.episode.episodeQuotedAsset.balance.value = sessionParameters.sessionQuotedAsset.config.initialBalance + + /* Recording the opening at the Trading Engine Data Structure */ + tradingEngine.current.episode.status.value = 'Open' + tradingEngine.current.episode.serialNumber.value = 1 + tradingEngine.current.episode.identifier.value = global.UNIQUE_ID() + tradingEngine.current.episode.begin.value = tradingEngine.current.episode.candle.begin.value + tradingEngine.current.episode.beginRate.value = tradingEngine.current.episode.candle.close.value + } + } + + function updateExitType(exitType) { + tradingEngine.current.episode.exitType.value = exitType + } + + function closeEpisode() { + tradingEngine.current.episode.status.value = 'Closed' + tradingEngine.current.episode.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.episode.endRate.value = tradingEngine.current.episode.candle.close.value + tradingEngine.current.episode.episodeBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngine.current.episode.episodeQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value + } + + function resetEpisode() { + tradingEngineModule.initializeNode(tradingEngine.current.episode) + } + + function updateEnds() { + if (tradingEngine.current.episode.status.value === 'Open') { + tradingEngine.current.episode.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.episode.endRate.value = tradingEngine.current.episode.candle.close.value + tradingEngine.current.episode.episodeBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngine.current.episode.episodeQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value + } + } + + function resetTradingEngineDataStructure() { + if (tradingEngine.current.episode.status.value === 'Closed') { + resetEpisode() + } + } + + function updateCounters() { + if (tradingEngine.current.episode.status.value === 'Open') { + tradingEngine.current.episode.episodeCounters.periods.value++ + } + } + + function updateStatistics() { + + /* Daus Calculation */ + tradingEngine.current.episode.episodeStatistics.days.value = + tradingEngine.current.episode.episodeCounters.periods.value * + sessionParameters.timeFrame.config.value / + global.ONE_DAY_IN_MILISECONDS + } + + function cycleBasedStatistics() { + + calculateAssetsStatistics() + calculateEpisodeStatistics() + + function calculateAssetsStatistics() { + /* Updating Profit Loss */ + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value - + sessionParameters.sessionBaseAsset.config.initialBalance + + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value - + sessionParameters.sessionQuotedAsset.config.initialBalance + + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.profitLoss.value, 10) + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value, 10) + + /* + Updating ROI + + https://www.investopedia.com/articles/basics/10/guide-to-calculating-roi.asp + */ + tradingEngine.current.episode.episodeBaseAsset.ROI.value = + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value / + tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * 100 + + tradingEngine.current.episode.episodeQuotedAsset.ROI.value = + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value / + tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value * 100 + + tradingEngine.current.episode.episodeBaseAsset.ROI.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.ROI.value, 10) + tradingEngine.current.episode.episodeQuotedAsset.ROI.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.ROI.value, 10) + + /* Updating Hit Ratio */ + if (tradingEngine.current.episode.episodeCounters.positions.value > 0) { + tradingEngine.current.episode.episodeBaseAsset.hitRatio.value = + tradingEngine.current.episode.episodeBaseAsset.hits.value / + tradingEngine.current.episode.episodeCounters.positions.value + + tradingEngine.current.episode.episodeQuotedAsset.hitRatio.value = + tradingEngine.current.episode.episodeQuotedAsset.hits.value / + tradingEngine.current.episode.episodeCounters.positions.value + + tradingEngine.current.episode.episodeBaseAsset.hitRatio.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.hitRatio.value, 10) + tradingEngine.current.episode.episodeQuotedAsset.hitRatio.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.hitRatio.value, 10) + } + + /* + Updating Annualized Rate Of Return + + https://www.investopedia.com/terms/a/annualized-rate.asp + */ + tradingEngine.current.episode.episodeBaseAsset.annualizedRateOfReturn.value = + Math.pow( + ( + tradingEngine.current.episode.episodeBaseAsset.beginBalance.value + + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value + ) / tradingEngine.current.episode.episodeBaseAsset.beginBalance.value + , + (365 / tradingEngine.current.episode.episodeStatistics.days.value) + ) - 1 + + tradingEngine.current.episode.episodeQuotedAsset.annualizedRateOfReturn.value = + Math.pow( + ( + tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value + + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value + ) / tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value + , + (365 / tradingEngine.current.episode.episodeStatistics.days.value) + ) - 1 + + tradingEngine.current.episode.episodeBaseAsset.annualizedRateOfReturn.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.annualizedRateOfReturn.value, 10) + tradingEngine.current.episode.episodeQuotedAsset.annualizedRateOfReturn.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.annualizedRateOfReturn.value, 10) + + /* Updating Hit or Fail */ + if (tradingEngine.current.episode.episodeBaseAsset.profitLoss.value > 0) { + tradingEngine.current.episode.episodeBaseAsset.hitFail.value = 'Hit' + } else { + tradingEngine.current.episode.episodeBaseAsset.hitFail.value = 'Fail' + } + if (tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value > 0) { + tradingEngine.current.episode.episodeQuotedAsset.hitFail.value = 'Hit' + } else { + tradingEngine.current.episode.episodeQuotedAsset.hitFail.value = 'Fail' + } + } + + function calculateEpisodeStatistics() { + /* Updating Profit Loss */ + tradingEngine.current.episode.episodeStatistics.profitLoss.value = + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.episode.candle.close.value + + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value + + tradingEngine.current.episode.episodeStatistics.profitLoss.value = global.PRECISE(tradingEngine.current.episode.episodeStatistics.profitLoss.value, 10) + + /* + Updating ROI + + https://www.investopedia.com/articles/basics/10/guide-to-calculating-roi.asp + */ + tradingEngine.current.episode.episodeStatistics.ROI.value = + ( + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.episode.endRate.value + + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value + ) / ( + tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * tradingEngine.current.episode.beginRate.value + + tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value + ) * 100 + + tradingEngine.current.episode.episodeStatistics.ROI.value = global.PRECISE(tradingEngine.current.episode.episodeStatistics.ROI.value, 10) + + /* + Updating Annualized Rate Of Return + + https://www.investopedia.com/terms/a/annualized-rate.asp + */ + tradingEngine.current.episode.episodeStatistics.annualizedRateOfReturn.value = + Math.pow( + ( + tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * tradingEngine.current.episode.beginRate.value + + tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value + + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value + + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value + ) / + ( + tradingEngine.current.episode.episodeBaseAsset.beginBalance.value * tradingEngine.current.episode.beginRate.value + + tradingEngine.current.episode.episodeQuotedAsset.beginBalance.value + ) + , + (365 / tradingEngine.current.episode.episodeStatistics.days.value) + ) - 1 + + tradingEngine.current.episode.episodeStatistics.annualizedRateOfReturn.value = global.PRECISE(tradingEngine.current.episode.episodeStatistics.annualizedRateOfReturn.value, 10) + + /* Updating Hit or Fail */ + if (tradingEngine.current.episode.episodeStatistics.profitLoss.value > 0) { + tradingEngine.current.episode.episodeStatistics.hitFail.value = 'Hit' + } else { + tradingEngine.current.episode.episodeStatistics.hitFail.value = 'Fail' + } + } + } + + function updateDistanceToEventsCounters() { + /* Keeping Distance Counters Up-to-date while avoinding counting before the first event happens. */ + if ( + tradingEngine.current.episode.distanceToEvent.triggerOn.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.triggerOn.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.triggerOff.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.triggerOff.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.takePosition.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.takePosition.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.closePosition.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.closePosition.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.nextPhase.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.nextPhase.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.moveToPhase.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.moveToPhase.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.createOrder.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.createOrder.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.cancelOrder.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.cancelOrder.value++ + } + + if ( + tradingEngine.current.episode.distanceToEvent.closeOrder.value > 0 + ) { + tradingEngine.current.episode.distanceToEvent.closeOrder.value++ + } + } +} \ No newline at end of file diff --git a/LowFrequencyTrading/TradingExecution.js b/LowFrequencyTrading/TradingExecution.js index b588689b7..7e6619ec3 100644 --- a/LowFrequencyTrading/TradingExecution.js +++ b/LowFrequencyTrading/TradingExecution.js @@ -1,11 +1,13 @@ exports.newTradingExecution = function newTradingExecution(bot, logger, tradingEngineModule) { /* - The Trading Execution modules manages the execution of orders against the exchanges. + The Trading Execution modules manages the execution nodes and execution algoritms. */ const MODULE_NAME = 'Trading Execution' let thisObject = { - checkExecution: checkExecution, + mantain: mantain, + reset: reset, + runExecution: runExecution, initialize: initialize, finalize: finalize } @@ -14,11 +16,8 @@ exports.newTradingExecution = function newTradingExecution(bot, logger, tradingE let tradingSystem let sessionParameters - const EXCHANGE_API_MODULE = require('./ExchangeAPI.js') - let exchangeAPIModule = EXCHANGE_API_MODULE.newExchangeAPI(bot, logger) - - const ANNOUNCEMENTS_MODULE = require('./Announcements.js') - let announcementsModule = ANNOUNCEMENTS_MODULE.newAnnouncements(bot, logger) + const TRADING_ORDERS_MODULE = require('./TradingOrders.js') + let tradingOrdersModule = TRADING_ORDERS_MODULE.newTradingOrders(bot, logger, tradingEngineModule) return thisObject @@ -27,8 +26,7 @@ exports.newTradingExecution = function newTradingExecution(bot, logger, tradingE tradingEngine = bot.simulationState.tradingEngine sessionParameters = bot.SESSION.parameters - exchangeAPIModule.initialize() - announcementsModule.initialize() + tradingOrdersModule.initialize() } function finalize() { @@ -36,551 +34,70 @@ exports.newTradingExecution = function newTradingExecution(bot, logger, tradingE tradingEngine = undefined sessionParameters = undefined - exchangeAPIModule.finalize() - exchangeAPIModule = undefined - - announcementsModule.finalize() - announcementsModule = undefined + tradingOrdersModule.finalize() + tradingOrdersModule = undefined } - async function checkExecution(executionNode, stageIsOpening, stageIsClosing, stageSizeLimit, stageOrdersSize, stageFilledSize) { + function mantain() { + tradingOrdersModule.mantain() + } - /* Enforcing Precision Limit */ - stageSizeLimit.value = global.PRECISE(stageSizeLimit.value, 10) - stageOrdersSize.value = global.PRECISE(stageOrdersSize.value, 10) + function reset() { + tradingOrdersModule.reset() + } - await checkExecutionAlgorithms(executionNode) + async function runExecution( + executionNode, + tradingEngineStage + ) { + try { + /* Trading Engine Stage Validations */ + if (tradingEngineStage.stageBaseAsset === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset === undefined', tradingEngineStage) } + if (tradingEngineStage.stageBaseAsset.targetSize === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.targetSize === undefined', tradingEngineStage) } + if (tradingEngineStage.stageBaseAsset.sizePlaced === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.sizePlaced === undefined', tradingEngineStage) } + if (tradingEngineStage.stageBaseAsset.sizeFilled === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.sizeFilled === undefined', tradingEngineStage) } + if (tradingEngineStage.stageBaseAsset.feesPaid === undefined) { badDefinitionUnhandledException(undefined, 'stageBaseAsset.feesPaid === undefined', tradingEngineStage) } + if (tradingEngineStage.stageQuotedAsset === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset === undefined', tradingEngineStage) } + if (tradingEngineStage.stageQuotedAsset.targetSize === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.targetSize === undefined', tradingEngineStage) } + if (tradingEngineStage.stageQuotedAsset.sizePlaced === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.sizePlaced === undefined', tradingEngineStage) } + if (tradingEngineStage.stageQuotedAsset.sizeFilled === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.sizeFilled === undefined', tradingEngineStage) } + if (tradingEngineStage.stageQuotedAsset.feesPaid === undefined) { badDefinitionUnhandledException(undefined, 'stageQuotedAsset.feesPaid === undefined', tradingEngineStage) } - async function checkExecutionAlgorithms(executionNode) { - for (let i = 0; i < executionNode.executionAlgorithms.length; i++) { - let executionAlgorithm = executionNode.executionAlgorithms[i] - await checkOrders(executionAlgorithm.marketBuyOrders, executionAlgorithm, executionNode) - await checkOrders(executionAlgorithm.marketSellOrders, executionAlgorithm, executionNode) - await checkOrders(executionAlgorithm.limitBuyOrders, executionAlgorithm, executionNode) - await checkOrders(executionAlgorithm.limitSellOrders, executionAlgorithm, executionNode) + await checkExecutionAlgorithms(executionNode) + + async function checkExecutionAlgorithms(executionNode) { + for (let i = 0; i < executionNode.executionAlgorithms.length; i++) { + let executionAlgorithm = executionNode.executionAlgorithms[i] + await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.marketBuyOrders, executionAlgorithm, executionNode) + await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.marketSellOrders, executionAlgorithm, executionNode) + await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.limitBuyOrders, executionAlgorithm, executionNode) + await tradingOrdersModule.checkOrders(tradingEngineStage, executionAlgorithm.limitSellOrders, executionAlgorithm, executionNode) + } + } + + } catch (err) { + tradingSystem.errors.push([executionNode.id, err.message]) + + if (typeof err === 'string' || err instanceof String) { + logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err) + } + if (err.stack !== undefined) { + logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err.stack) } } + } - async function checkOrders(orders, executionAlgorithm, executionNode) { - for (let i = 0; i < orders.length; i++) { + function badDefinitionUnhandledException(err, message, node) { + tradingSystem.errors.push([node.id, message]) - let tradingSystemOrder = orders[i] - - /* Trading System Validations */ - if (tradingSystemOrder.config.positionSizePercentage === undefined) { continue } - if (tradingSystemOrder.referenceParent === undefined) { continue } - - let tradingEngineOrder = tradingEngineModule.getNodeById(tradingSystemOrder.referenceParent.id) - - /* Trading Engine Validations */ - if (tradingEngineOrder.serialNumber === undefined) { continue } - if (tradingEngineOrder.identifier === undefined) { continue } - if (tradingEngineOrder.exchangeId === undefined) { continue } - if (tradingEngineOrder.begin === undefined) { continue } - if (tradingEngineOrder.end === undefined) { continue } - if (tradingEngineOrder.rate === undefined) { continue } - if (tradingEngineOrder.size === undefined) { continue } - if (tradingEngineOrder.status === undefined) { continue } - if (tradingEngineOrder.algorithmName === undefined) { continue } - if (tradingEngineOrder.orderCounters === undefined) { continue } - if (tradingEngineOrder.orderCounters.periods === undefined) { continue } - if (tradingEngineOrder.orderStatistics.days === undefined) { continue } - if (tradingEngineOrder.orderStatistics.percentageFilled === undefined) { continue } - if (tradingEngineOrder.orderStatistics.actualRate === undefined) { continue } - if (tradingEngineOrder.orderStatistics.feesPaid === undefined) { continue } - - switch (tradingEngineOrder.status.value) { - case 'Not Open': { - { - /* Check if we need to Create this Order */ - if (stageIsClosing === true) { continue } - let situationName = checkOrderEvent(tradingSystemOrder.createOrderEvent, tradingSystemOrder, executionAlgorithm, executionNode) - if (situationName !== undefined) { - - /* Open a new order */ - await openOrder(executionAlgorithm, tradingSystemOrder, tradingEngineOrder, situationName) - } - } - break - } - case 'Open': { - tradingEngineOrder.end.value = tradingEngine.current.candle.end.value - tradingEngineOrder.orderCounters.periods.value++ - tradingEngineOrder.orderStatistics.days.value = tradingEngineOrder.orderCounters.periods.value * sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS - - /* Simulate Events that happens at the Exchange, if needed. */ - simulateExchangeEvents(tradingSystemOrder, tradingEngineOrder) - - /* Check Events that happens at the Exchange, if needed. */ - await checkExchangeEvents(tradingSystemOrder, tradingEngineOrder) - - /* If the stage is closing or the order is not still Open, we wont be cancelling orders based on defined events */ - if (stageIsClosing !== true && tradingEngineOrder.status.value === 'Open') { - - /* Check if we need to Cancel this Order */ - let situationName = checkOrderEvent(tradingSystemOrder.cancelOrderEvent, tradingSystemOrder, executionAlgorithm, executionNode) - if (situationName !== undefined) { - simulateCancelOrder(tradingSystemOrder, tradingEngineOrder, 'Cancel Event') - await exchangeCancelOrder(tradingSystemOrder, tradingEngineOrder, 'Cancel Event') - } - } - } - } - } - } - - async function openOrder(executionAlgorithm, tradingSystemOrder, tradingEngineOrder, situationName) { - /* Order Size Calculation */ - tradingEngineOrder.size.value = tradingSystem.formulas.get(executionAlgorithm.positionSize.formula.id) * tradingSystemOrder.config.positionSizePercentage / 100 - if (stageOrdersSize.value + tradingEngineOrder.size.value > stageSizeLimit.value) { - /* We reduce the size to the remaining size of the position. */ - tradingEngineOrder.size.value = stageSizeLimit.value - stageOrdersSize.value - tradingEngineOrder.size.value = global.PRECISE(tradingEngineOrder.size.value, 10) - } - - /* Check Order Size */ - if (tradingEngineOrder.size.value <= 0) { return } - - /* Order Rate Calculation */ - tradingEngineOrder.rate.value = tradingEngine.current.position.rate.value // By default this is the order rate. - if (tradingSystemOrder.positionRate !== undefined) { - if (tradingSystemOrder.positionRate.formula !== undefined) { - tradingEngineOrder.rate.value = tradingSystem.formulas.get(tradingSystemOrder.positionRate.formula.id) - tradingEngineOrder.rate.value = global.PRECISE(tradingEngineOrder.rate.value, 10) - } - } - - /* Place Order at the Exchange */ - let result = await createOrderAtExchange(tradingSystemOrder, tradingEngineOrder) - if (result !== true) { return } - - /* Updating Episode Counters */ - tradingEngine.episode.episodeCounters.orders.value++ - - /* Initialize this */ - tradingEngine.current.distanceToEvent.createOrder.value = 1 - - /* Create Order Procedure */ - tradingEngineOrder.status.value = 'Open' - tradingEngineOrder.identifier.value = global.UNIQUE_ID() - tradingEngineOrder.begin.value = tradingEngine.current.candle.begin.value - tradingEngineOrder.end.value = tradingEngine.current.candle.end.value - tradingEngineOrder.serialNumber.value = tradingEngine.episode.episodeCounters.orders.value - tradingEngineOrder.orderName.value = tradingSystemOrder.name - tradingEngineOrder.algorithmName.value = executionAlgorithm.name - tradingEngineOrder.situationName.value = situationName - - /* Update Stage Orders Size */ - stageOrdersSize.value = stageOrdersSize.value + tradingEngineOrder.size.value - stageOrdersSize.value = global.PRECISE(stageOrdersSize.value, 10) - } - - async function createOrderAtExchange(tradingSystemOrder, tradingEngineOrder) { - - /* Filter by Session Type */ - switch (bot.SESSION.type) { - case 'Backtesting Session': { - return true - } - case 'Live Trading Session': { - break - } - case 'Fordward Testing Session': { - break - } - case 'Paper Trading Session': { - return true - } - } - - let orderId = await exchangeAPIModule.createOrder(tradingSystemOrder, tradingEngineOrder) - - if (orderId !== undefined) { - tradingEngineOrder.exchangeId.value = orderId - return true - } - } - - function simulateExchangeEvents(tradingSystemOrder, tradingEngineOrder) { - - /* Filter by Session Type */ - switch (bot.SESSION.type) { - case 'Backtesting Session': { - break - } - case 'Live Trading Session': { - return - } - case 'Fordward Testing Session': { - return - } - case 'Paper Trading Session': { - break - } - } - - /* Filter by what is defined at the Strategy */ - if (tradingSystemOrder.simulatedExchangeEvents === undefined) { return } - - let previousSizeFilled = tradingEngineOrder.orderStatistics.sizeFilled.value - let previousPercentageFilled = tradingEngineOrder.orderStatistics.percentageFilled.value - let previousAmountReceived = tradingEngineOrder.orderStatistics.amountReceived.value - let previousFeesPaid = tradingEngineOrder.orderStatistics.feesPaid.value - - actualRateSimulation() - feesPaidSimulation() - orderFillingSimulation() - sizeFilledSimulation() - amountReceivedSimulation() - - doTheAccounting(tradingSystemOrder, tradingEngineOrder, previousSizeFilled, previousPercentageFilled, previousAmountReceived, previousFeesPaid) - - /* If the Stage is Closing and this order is still open, we need to cancel it now */ - if (stageIsClosing === true && tradingEngineOrder.status.value !== 'Closed') { - simulateCancelOrder(tradingSystemOrder, tradingEngineOrder, 'Closing Stage') - } - - function actualRateSimulation() { - /* Actual Rate Simulation */ - if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate !== undefined) { - if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula !== undefined) { - /* Calculate this only once for this order */ - if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.orderStatistics.actualRate.config.initialValue) { - tradingEngineOrder.orderStatistics.actualRate.value = tradingSystem.formulas.get(tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula.id) - } - } - } else { - switch (tradingEngineOrder.type) { - case 'Market Order': { - /* Actual Rate is simulated based on the Session Paremeters */ - let slippageAmount = tradingEngineOrder.rate.value * bot.SESSION.parameters.slippage.config.positionRate / 100 - switch (tradingSystemOrder.type) { - case 'Market Sell Order': { - tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value - slippageAmount - break - } - case 'Market Buy Order': { - tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value + slippageAmount - break - } - } - break - } - case 'Limit Order': { - /* In Limit Orders the actual rate is the rate of the order, there is no slippage */ - tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value - break - } - } - } - tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10) - } - - function feesPaidSimulation() { - /* Fees Paid Simulation */ - if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid !== undefined) { - if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage !== undefined) { - if (tradingEngineOrder.orderStatistics.feesPaid.value === tradingEngineOrder.orderStatistics.feesPaid.config.initialValue) { - tradingEngineOrder.orderStatistics.feesPaid.value = tradingEngineOrder.size.value * tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage / 100 - } - } - } else { - /* Fees are simulated based on the Session Paremeters */ - switch (tradingEngineOrder.type) { - case 'Market Order': { - tradingEngineOrder.orderStatistics.feesPaid.value = tradingEngineOrder.size.value * bot.SESSION.parameters.feeStructure.config.taker / 100 - break - } - case 'Limit Order': { - tradingEngineOrder.orderStatistics.feesPaid.value = tradingEngineOrder.size.value * bot.SESSION.parameters.feeStructure.config.maker / 100 - break - } - } - } - tradingEngineOrder.orderStatistics.feesPaid.value = global.PRECISE(tradingEngineOrder.orderStatistics.feesPaid.value, 10) - } - - function orderFillingSimulation() { - /* Order Filling Simulation */ - if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill !== undefined) { - if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability !== undefined) { - - /* Percentage Filled */ - let percentageFilled = tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability * 100 - if (tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled > 100) { - percentageFilled = 100 - tradingEngineOrder.orderStatistics.percentageFilled.value - } - tradingEngineOrder.orderStatistics.percentageFilled.value = tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled - tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10) - - /* Check if we need to close */ - if (tradingEngineOrder.orderStatistics.percentageFilled.value === 100) { - - /* Close this Order */ - tradingEngineOrder.status.value = 'Closed' - tradingEngineOrder.exitType.value = 'Filled' - - /* Initialize this */ - tradingEngine.current.distanceToEvent.closeOrder.value = 1 - } - } - } - } - - function sizeFilledSimulation() { - /* Size Filled */ - tradingEngineOrder.orderStatistics.sizeFilled.value = tradingEngineOrder.size.value * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 - tradingEngineOrder.orderStatistics.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.sizeFilled.value, 10) - } - - function amountReceivedSimulation() { - /* Amount Received */ - tradingEngineOrder.orderStatistics.amountReceived.value = tradingEngineOrder.orderStatistics.sizeFilled.value * tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate - tradingEngineOrder.orderStatistics.amountReceived.value = global.PRECISE(tradingEngineOrder.orderStatistics.amountReceived.value, 10) - } - } - - async function checkExchangeEvents(tradingSystemOrder, tradingEngineOrder) { - - /* Filter by Session Type */ - switch (bot.SESSION.type) { - case 'Backtesting Session': { - return true - } - case 'Live Trading Session': { - break - } - case 'Fordward Testing Session': { - break - } - case 'Paper Trading Session': { - return true - } - } - - let order = await exchangeAPIModule.getOrder(tradingSystemOrder, tradingEngineOrder) - - if (order === undefined) { return } - - const AT_EXCHANGE_STATUS = { - OPEN: 'open', - CLOSED: 'closed', - CANCELLED: 'canceled' - } - - /* Status Checks */ - if (order.remaining === 0 && order.status === AT_EXCHANGE_STATUS.CLOSED) { - - /* Close this Order */ - tradingEngineOrder.status.value = 'Closed' - tradingEngineOrder.exitType.value = 'Filled' - - /* Initialize this */ - tradingEngine.current.distanceToEvent.closeOrder.value = 1 - } - if (order.remaining > 0 && order.status === AT_EXCHANGE_STATUS.CLOSED) { - - /* Close this Order */ - tradingEngineOrder.status.value = 'Closed' - tradingEngineOrder.exitType.value = 'Closed at the Exchange' - - /* Initialize this */ - tradingEngine.current.distanceToEvent.closeOrder.value = 1 - } - if (order.status === AT_EXCHANGE_STATUS.CANCELLED) { - - /* Close this Order */ - tradingEngineOrder.status.value = 'Closed' - tradingEngineOrder.exitType.value = 'Cancelled at the Exchange' - - /* Initialize this */ - tradingEngine.current.distanceToEvent.closeOrder.value = 1 - } - - syncWithExchange(tradingSystemOrder, tradingEngineOrder, order) - - /* Forced Cancellation Check */ - if (stageIsClosing === true && tradingEngineOrder.status.value !== 'Closed') { - await exchangeCancelOrder(tradingSystemOrder, tradingEngineOrder, 'Closing Stage') - } - } - - function syncWithExchange(tradingSystemOrder, tradingEngineOrder, order) { - - let previousSizeFilled = tradingEngineOrder.orderStatistics.sizeFilled.value - let previousPercentageFilled = tradingEngineOrder.orderStatistics.percentageFilled.value - let previousAmountReceived = tradingEngineOrder.orderStatistics.amountReceived.value - let previousFeesPaid = tradingEngineOrder.orderStatistics.feesPaid.value - - /* Fees Paid */ - tradingEngineOrder.orderStatistics.feesPaid.value = tradingEngineOrder.size.value - order.amount - tradingEngineOrder.orderStatistics.feesPaid.value = global.PRECISE(tradingEngineOrder.orderStatistics.feesPaid.value, 10) - - /* Percentage Filled */ - tradingEngineOrder.orderStatistics.percentageFilled.value = order.filled * 100 / (order.filled + order.remaining) - tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10) - - /* Size Filled */ - tradingEngineOrder.orderStatistics.sizeFilled.value = order.filled - tradingEngineOrder.orderStatistics.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.sizeFilled.value, 10) - - /* Amount Received */ - tradingEngineOrder.orderStatistics.amountReceived.value = order.cost - tradingEngineOrder.orderStatistics.amountReceived.value = global.PRECISE(tradingEngineOrder.orderStatistics.amountReceived.value, 10) - - /* Actual Rate */ - tradingEngineOrder.orderStatistics.actualRate.value = order.price - tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10) - - doTheAccounting(tradingSystemOrder, tradingEngineOrder, previousSizeFilled, previousPercentageFilled, previousAmountReceived, previousFeesPaid) - } - - function doTheAccounting(tradingSystemOrder, tradingEngineOrder, previousSizeFilled, previousPercentageFilled, previousAmountReceived, previousFeesPaid) { - - /* Unaccount for the previous Stage Filled Size */ - stageFilledSize.value = stageFilledSize.value - tradingEngineOrder.size.value * previousPercentageFilled / 100 - /* Account for the current Stage Filled Size */ - stageFilledSize.value = stageFilledSize.value + tradingEngineOrder.size.value * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 - stageFilledSize.value = global.PRECISE(stageFilledSize.value, 10) - - - /* Balances Update */ - switch (true) { - case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { - - /* Undo the previous accounting */ - tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value - previousFeesPaid - previousSizeFilled - /* Account the current filling and fees */ - tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value + tradingEngineOrder.orderStatistics.sizeFilled.value + tradingEngineOrder.orderStatistics.feesPaid.value - - /* Undo the previous accounting */ - tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.baseAsset.value + previousAmountReceived - /* Account the current filling and fees */ - tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.baseAsset.value - tradingEngineOrder.orderStatistics.amountReceived.value - - break - } - case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': { - - /* Undo the previous accounting */ - tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value + previousFeesPaid + previousSizeFilled - /* Account the current filling and fees */ - tradingEngine.current.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value - tradingEngineOrder.orderStatistics.sizeFilled.value - tradingEngineOrder.orderStatistics.feesPaid.value - - /* Undo the previous accounting */ - tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.baseAsset.value - previousAmountReceived - /* Account the current filling and fees */ - tradingEngine.current.balance.quotedAsset.value = tradingEngine.current.balance.baseAsset.value + tradingEngineOrder.orderStatistics.amountReceived.value - - break - } - } - } - - function simulateCancelOrder(tradingSystemOrder, tradingEngineOrder, exitType) { - - /* Filter by Session Type */ - switch (bot.SESSION.type) { - case 'Backtesting Session': { - break - } - case 'Live Trading Session': { - return - } - case 'Fordward Testing Session': { - return - } - case 'Paper Trading Session': { - break - } - } - - /* Close this Order */ - tradingEngineOrder.status.value = 'Closed' - tradingEngineOrder.exitType.value = exitType - - /* Initialize this */ - tradingEngine.current.distanceToEvent.closeOrder.value = 1 - - /* Since the order was cancelled, we remove the unfilled amount of the order from here */ - let unfilledPercentage = 100 - tradingEngineOrder.orderStatistics.percentageFilled.value - let unfilledSize = tradingEngineOrder.size.value * unfilledPercentage / 100 - stageOrdersSize.value = stageOrdersSize.value - unfilledSize - stageOrdersSize.value = global.PRECISE(stageOrdersSize.value, 10) - } - - async function exchangeCancelOrder(tradingSystemOrder, tradingEngineOrder, exitType) { - - /* Filter by Session Type */ - switch (bot.SESSION.type) { - case 'Backtesting Session': { - return - } - case 'Live Trading Session': { - break - } - case 'Fordward Testing Session': { - break - } - case 'Paper Trading Session': { - return - } - } - - /* Check if we can cancel the order at the Exchange. */ - let result = await exchangeAPIModule.cancelOrder(tradingSystemOrder, tradingEngineOrder) - if (result === true) { - /* Close this Order */ - tradingEngineOrder.status.value = 'Closed' - tradingEngineOrder.exitType.value = exitType - - /* Initialize this */ - tradingEngine.current.distanceToEvent.closeOrder.value = 1 - - /* Since the order was cancelled, we remove the unfilled amount of the order from here */ - let unfilledPercentage = 100 - tradingEngineOrder.orderStatistics.percentageFilled.value - let unfilledSize = tradingEngineOrder.size.value * unfilledPercentage / 100 - stageOrdersSize.value = stageOrdersSize.value - unfilledSize - stageOrdersSize.value = global.PRECISE(stageOrdersSize.value, 10) - - /* - Perhaps the order was filled a bit more between the last time we checked and when it was cancelled. - To sync our accounting, we need to check the order one last time and if it changed, fix it. - */ - - let order = await exchangeAPIModule.getOrder(tradingSystemOrder, tradingEngineOrder) - - if (order === undefined) { return } - - syncWithExchange(tradingSystemOrder, tradingEngineOrder, order) - } - } - - function checkOrderEvent(event, order, executionAlgorithm, executionNode) { - if (event !== undefined) { - for (let k = 0; k < event.situations.length; k++) { - let situation = event.situations[k] - let passed - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(event.id) - tradingSystem.highlights.push(order.id) - tradingSystem.highlights.push(executionAlgorithm.id) - tradingSystem.highlights.push(executionNode.id) - - announcementsModule.makeAnnoucements(event) - return situation.name // if the event is triggered, we return the name of the situation that passed - } - } - } + logger.write(MODULE_NAME, "[ERROR] -> " + message); + logger.write(MODULE_NAME, "[ERROR] -> node.name = " + node.name); + logger.write(MODULE_NAME, "[ERROR] -> node.type = " + node.type); + logger.write(MODULE_NAME, "[ERROR] -> node.config = " + JSON.stringify(node.config)); + if (err !== undefined) { + logger.write(MODULE_NAME, "[ERROR] -> err.stack = " + err.stack); } + throw 'Please fix the problem and try again.' } } diff --git a/LowFrequencyTrading/TradingOrders.js b/LowFrequencyTrading/TradingOrders.js new file mode 100644 index 000000000..76d1cabe0 --- /dev/null +++ b/LowFrequencyTrading/TradingOrders.js @@ -0,0 +1,1292 @@ +exports.newTradingOrders = function newTradingOrders(bot, logger, tradingEngineModule) { + /* + The Trading Orders modules manages the execution of orders against the exchanges. + */ + const MODULE_NAME = 'Trading Orders' + + let thisObject = { + mantain: mantain, + reset: reset, + checkOrders: checkOrders, + initialize: initialize, + finalize: finalize + } + + let tradingEngine + let tradingSystem + let sessionParameters + + const EXCHANGE_API_MODULE = require('./ExchangeAPI.js') + let exchangeAPIModule = EXCHANGE_API_MODULE.newExchangeAPI(bot, logger) + + const ANNOUNCEMENTS_MODULE = require('./Announcements.js') + let announcementsModule = ANNOUNCEMENTS_MODULE.newAnnouncements(bot, logger) + + return thisObject + + function initialize() { + tradingSystem = bot.simulationState.tradingSystem + tradingEngine = bot.simulationState.tradingEngine + sessionParameters = bot.SESSION.parameters + + exchangeAPIModule.initialize() + announcementsModule.initialize() + } + + function finalize() { + tradingSystem = undefined + tradingEngine = undefined + sessionParameters = undefined + + exchangeAPIModule.finalize() + exchangeAPIModule = undefined + + announcementsModule.finalize() + announcementsModule = undefined + } + + function mantain() { + mantainReset(true, false) + } + + function reset() { + mantainReset(false, true) + } + + function mantainReset(mantain, reset) { + + if (tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue) { return } + + let stageNode + let executionNode + + stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage + executionNode = stageNode.openExecution + processExecutionNode(executionNode, tradingEngine.current.strategyOpenStage.status.value) + + stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].closeStage + executionNode = stageNode.closeExecution + processExecutionNode(executionNode, tradingEngine.current.strategyCloseStage.status.value) + + function processExecutionNode(executionNode, stageStatus) { + + preocessExecutionAlgorithms(executionNode) + + function preocessExecutionAlgorithms(executionNode) { + for (let i = 0; i < executionNode.executionAlgorithms.length; i++) { + let executionAlgorithm = executionNode.executionAlgorithms[i] + processOrders(executionAlgorithm.marketBuyOrders) + processOrders(executionAlgorithm.marketSellOrders) + processOrders(executionAlgorithm.limitBuyOrders) + processOrders(executionAlgorithm.limitSellOrders) + } + } + + function processOrders(orders) { + for (let i = 0; i < orders.length; i++) { + let tradingSystemOrder = orders[i] + if (tradingSystemOrder.referenceParent === undefined) { + badDefinitionUnhandledException(undefined, 'tradingSystemOrder.referenceParent === undefined', tradingSystemOrder) + } + let tradingEngineOrder = tradingEngineModule.getNodeById(tradingSystemOrder.referenceParent.id) + + if (tradingEngineOrder.status === undefined) { + badDefinitionUnhandledException(undefined, 'tradingEngineOrder.status === undefined', tradingEngineOrder) + } + + if (mantain === true) { + updateCounters(tradingEngineOrder) + updateEnds(tradingEngineOrder) + updateStatistics(tradingEngineOrder) + } + if (reset === true) { + resetTradingEngineDataStructure(tradingEngineOrder, tradingSystemOrder, stageStatus) + } + } + } + } + } + + async function checkOrders(tradingEngineStage, orders, executionAlgorithm, executionNode) { + for (let i = 0; i < orders.length; i++) { + + let tradingSystemOrder = orders[i] + tradingSystemValidations(tradingSystemOrder) + let tradingEngineOrder = tradingEngineModule.getNodeById(tradingSystemOrder.referenceParent.id) + tradingEngineValidations(tradingEngineOrder) + + switch (tradingEngineOrder.status.value) { + case 'Not Open': { + { + /* During the First cycle we can not create new orders. That is reserved for the Second cycle. */ + if (tradingEngine.current.episode.cycle.value === 'First') { continue } + /* When the stage is closing we can not create new orders */ + if (tradingEngineStage.status.value === 'Closing') { continue } + /* + Check if we can create an order based on the config value for spawnMultipleOrders. + Trading System Orders that cannot spawn more than one Trading Engine Order needs to check if + at the Trading Engine Order the lock is Open or Closed. + */ + if (tradingSystemOrder.config.spawnMultipleOrders !== true) { + if (tradingEngineOrder.lock.value === 'Closed') { + tradingSystem.infos.push(['Order ' + tradingSystemOrder.name + ' skipped because lock was closed.']) + continue + } + } + /* Check if we need to Create this Order */ + let situationName = await checkOrderEvent(tradingSystemOrder.createOrderEvent, tradingSystemOrder, executionAlgorithm, executionNode) + if (situationName !== undefined) { + + /* Open a new order */ + await tryToOpenOrder(tradingEngineStage, executionAlgorithm, tradingSystemOrder, tradingEngineOrder, situationName) + } + } + break + } + case 'Open': { + /* During the Second cycle we can not cancel orders. That is reserved for the First cycle. */ + if (tradingEngine.current.episode.cycle.value === 'Second') { continue } + + /* Simulate Events that happens at the Exchange, if needed. */ + simulateExchangeEvents(tradingEngineStage, tradingSystemOrder, tradingEngineOrder) + + /* Check Events that happens at the Exchange, if needed. */ + let allGood = await checkExchangeEvents(tradingEngineStage, tradingSystemOrder, tradingEngineOrder) + + if (allGood !== true) { + /* + For some reason we could not check the order at the exchange, so we will not even check if we + need to cancel it, since we could end up with inconsisten information at the accounting level. + */ + if (tradingSystemOrder.cancelOrderEvent !== undefined) { + tradingSystem.warnings.push([tradingSystemOrder.cancelOrderEvent.id, 'Skipping Cancel Event Checking because cheking the order at the exchange failed.']) + } + return + } + + /* Check if we need to cancel the order */ + await checkCancelOrderEvent(tradingEngineStage, executionAlgorithm, executionNode, tradingEngineOrder, tradingSystemOrder) + /* + If by this time the order is closed, we need to clone it and get the close + to the Last node at the Trading Engine data structure. + */ + if (tradingEngineOrder.status.value === 'Closed') { + switch (tradingEngineOrder.type) { + case 'Market Order': { + tradingEngineModule.cloneValues(tradingEngineOrder, tradingEngine.last.marketOrders) + break + } + case 'Limit Order': { + tradingEngineModule.cloneValues(tradingEngineOrder, tradingEngine.last.limitOrders) + break + } + } + } + } + } + } + } + + async function checkCancelOrderEvent(tradingEngineStage, executionAlgorithm, executionNode, tradingEngineOrder, tradingSystemOrder) { + /* + In the previous steps, we might have discovered that the order was cancelled + at the exchange, or filled, so the order might still not be Open. + If the stage is closing or the order is not Open, we wont be cancelling orders + based on defined events. + */ + if (tradingEngineStage.status.value !== 'Closing' && tradingEngineOrder.status.value === 'Open') { + + /* Check if we need to Cancel this Order */ + let situationName = await checkOrderEvent(tradingSystemOrder.cancelOrderEvent, tradingSystemOrder, executionAlgorithm, executionNode) + if (situationName !== undefined) { + + /* Simulate Order Cancelation, if needed. */ + simulateCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, 'Cancel Event') + + /* Cancel the order at the Exchange, if needed. */ + await exchangeCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, 'Cancel Event') + + await updateEndsWithCycle(tradingEngineOrder) + } + } + } + + function tradingSystemValidations(tradingSystemOrder) { + /* Trading System Validations */ + if (tradingSystemOrder.config.percentageOfAlgorithmSize === undefined) { badDefinitionUnhandledException(undefined, 'tradingSystemOrder.config.percentageOfAlgorithmSize === undefined', tradingSystemOrder) } + if (tradingSystemOrder.referenceParent === undefined) { badDefinitionUnhandledException(undefined, 'tradingSystemOrder.referenceParent === undefined', tradingSystemOrder) } + } + + function tradingEngineValidations(tradingEngineOrder) { + /* Trading Engine Order Validations */ + if (tradingEngineOrder.serialNumber === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.serialNumber === undefined', tradingEngineOrder) } + if (tradingEngineOrder.identifier === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.identifier === undefined', tradingEngineOrder) } + if (tradingEngineOrder.exchangeId === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.exchangeId === undefined', tradingEngineOrder) } + if (tradingEngineOrder.begin === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.begin === undefined', tradingEngineOrder) } + if (tradingEngineOrder.end === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.end === undefined', tradingEngineOrder) } + if (tradingEngineOrder.rate === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.rate === undefined', tradingEngineOrder) } + if (tradingEngineOrder.status === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.status === undefined', tradingEngineOrder) } + if (tradingEngineOrder.algorithmName === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.algorithmName === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderCounters === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderCounters === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderCounters.periods === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderCounters.periods === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderStatistics.days === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderStatistics.days === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderStatistics.percentageFilled === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderStatistics.percentageFilled === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderStatistics.actualRate === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderStatistics.actualRate === undefined', tradingEngineOrder) } + + if (tradingEngineOrder.orderBaseAsset.size === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderBaseAsset.size === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderBaseAsset.sizeFilled === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderBaseAsset.sizeFilled === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderBaseAsset.feesPaid === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderBaseAsset.feesPaid === undefined', tradingEngineOrder) } + + if (tradingEngineOrder.orderQuotedAsset.size === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderQuotedAsset.size === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderQuotedAsset.sizeFilled === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderQuotedAsset.sizeFilled === undefined', tradingEngineOrder) } + if (tradingEngineOrder.orderQuotedAsset.feesPaid === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.orderQuotedAsset.feesPaid === undefined', tradingEngineOrder) } + } + + async function tryToOpenOrder(tradingEngineStage, executionAlgorithm, tradingSystemOrder, tradingEngineOrder, situationName) { + + await calculateOrderRate() + await calculateOrderSize() + + /* Check Size: We are not going to create Orders which size is equal or less to zero. */ + if (tradingEngineOrder.orderBaseAsset.size.value <= 0 || tradingEngineOrder.orderQuotedAsset.size.value <= 0) { + tradingSystem.warnings.push([tradingSystemOrder.id, 'Could not open this order because its size would be zero.']) + return + } + + /* Place Order at the Exchange, if needed. */ + let result = await createOrderAtExchange(tradingSystemOrder, tradingEngineOrder) + if (result !== true) { + tradingSystem.warnings.push([tradingSystemOrder.id, 'Could not open this order because something failed placing the order at the Exchange.']) + return + } + + /* Update Stage Placed Size */ + tradingEngineStage.stageBaseAsset.sizePlaced.value = tradingEngineStage.stageBaseAsset.sizePlaced.value + tradingEngineOrder.orderBaseAsset.size.value + tradingEngineStage.stageBaseAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageBaseAsset.sizePlaced.value, 10) + tradingEngineStage.stageQuotedAsset.sizePlaced.value = tradingEngineStage.stageQuotedAsset.sizePlaced.value + tradingEngineOrder.orderQuotedAsset.size.value + tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) + + /* Updating Episode Counters */ + tradingEngine.current.episode.episodeCounters.orders.value++ + + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.createOrder.value = 1 + + /* Create Order Procedure */ + tradingEngineOrder.status.value = 'Open' + tradingEngineOrder.identifier.value = global.UNIQUE_ID() + tradingEngineOrder.begin.value = tradingEngine.current.episode.cycle.begin.value + tradingEngineOrder.end.value = tradingEngine.current.episode.cycle.end.value + tradingEngineOrder.serialNumber.value = tradingEngine.current.episode.episodeCounters.orders.value + tradingEngineOrder.orderName.value = tradingSystemOrder.name + tradingEngineOrder.algorithmName.value = executionAlgorithm.name + tradingEngineOrder.situationName.value = situationName + + async function calculateOrderRate() { + + /* Optional Rate Definition */ + if (tradingSystemOrder.type === 'Limit Buy Order' || tradingSystemOrder.type === 'Limit Sell Order') { + if (tradingSystemOrder.orderRate === undefined) { badDefinitionUnhandledException(undefined, 'tradingSystemOrder.orderRate === undefined', tradingSystemOrder) } + if (tradingSystemOrder.orderRate.formula === undefined) { badDefinitionUnhandledException(undefined, 'tradingSystemOrder.orderRate.formula === undefined', tradingSystemOrder) } + + /* Extract the rate value from the user-defined formula */ + tradingEngineOrder.rate.value = tradingSystem.formulas.get(tradingSystemOrder.orderRate.formula.id) + + /* Final rate validations */ + if (tradingEngineOrder.rate.value === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.rate.value === undefined', tradingSystemOrder) } + if (isNaN(tradingEngineOrder.rate.value) === true) { badDefinitionUnhandledException(undefined, 'isNaN(tradingEngineOrder.rate.value) === true', tradingSystemOrder) } + if (tradingEngineOrder.rate.value <= 0) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.rate.value <= 0', tradingSystemOrder) } + + tradingEngineOrder.rate.value = global.PRECISE(tradingEngineOrder.rate.value, 10) + } else { + /* + For Market Orders, the rate is irrelevant, since it is not sent to the Exchange. + We store at this field the last know price as a reference. + */ + tradingEngineOrder.rate.value = tradingEngine.current.episode.candle.close.value + } + } + + async function calculateOrderSize() { + /* Validate that this config exists */ + if (executionAlgorithm.config.percentageOfStageTargetSize === undefined) { badDefinitionUnhandledException(undefined, 'executionAlgorithm.config.percentageOfStageTargetSize === undefined', executionAlgorithm) } + if (isNaN(executionAlgorithm.config.percentageOfStageTargetSize) === true) { badDefinitionUnhandledException(undefined, 'isNaN(executionAlgorithm.config.percentageOfStageTargetSize) === true', executionAlgorithm) } + + let algorithmSizeInBaseAsset = tradingEngineStage.stageBaseAsset.targetSize.value * executionAlgorithm.config.percentageOfStageTargetSize / 100 + let algorithmSizeInQuotedAsset = tradingEngineStage.stageQuotedAsset.targetSize.value * executionAlgorithm.config.percentageOfStageTargetSize / 100 + + /* Validate that this config exists */ + if (tradingSystemOrder.config.percentageOfAlgorithmSize === undefined) { badDefinitionUnhandledException(undefined, 'tradingSystemOrder.config.percentageOfAlgorithmSize === undefined', tradingSystemOrder) } + if (isNaN(tradingSystemOrder.config.percentageOfAlgorithmSize) === true) { badDefinitionUnhandledException(undefined, 'isNaN(tradingSystemOrder.config.percentageOfAlgorithmSize) === true', tradingSystemOrder) } + + /* + The Size calculation depends on how the user defined the size of the position. + The user could have defined the size of the position in Base Asset or Quoted Asset. + */ + switch (tradingEngineStage.stageDefinedIn.value) { + case 'Base Asset': { + /* Size in Base Asset */ + tradingEngineOrder.orderBaseAsset.size.value = algorithmSizeInBaseAsset * tradingSystemOrder.config.percentageOfAlgorithmSize / 100 + + /* Check that the Size calculated would not surpass Stage Target Size */ + if ( + tradingEngineOrder.orderBaseAsset.size.value + tradingEngineStage.stageBaseAsset.sizePlaced.value > + tradingEngineStage.stageBaseAsset.targetSize.value + ) { + tradingEngineOrder.orderBaseAsset.size.value = tradingEngineStage.stageBaseAsset.targetSize.value - tradingEngineStage.stageBaseAsset.sizePlaced.value + } + + /* Size in Quoted Asset */ + tradingEngineOrder.orderQuotedAsset.size.value = tradingEngineOrder.orderBaseAsset.size.value * tradingEngineOrder.rate.value + break + } + case 'Quoted Asset': { + /* Size in Quoted Asset */ + tradingEngineOrder.orderQuotedAsset.size.value = algorithmSizeInQuotedAsset * tradingSystemOrder.config.percentageOfAlgorithmSize / 100 + + /* Check that the Size calculated would not surpass Stage Target Size */ + if ( + tradingEngineOrder.orderQuotedAsset.size.value + tradingEngineStage.stageQuotedAsset.sizePlaced.value > + tradingEngineStage.stageQuotedAsset.targetSize.value + ) { + tradingEngineOrder.orderQuotedAsset.size.value = tradingEngineStage.stageQuotedAsset.targetSize.value - tradingEngineStage.stageQuotedAsset.sizePlaced.value + } + + /* Size in Base Asset */ + tradingEngineOrder.orderBaseAsset.size.value = tradingEngineOrder.orderQuotedAsset.size.value / tradingEngineOrder.rate.value + break + } + } + + tradingEngineOrder.orderBaseAsset.size.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.size.value, 10) + tradingEngineOrder.orderQuotedAsset.size.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.size.value, 10) + } + + async function createOrderAtExchange(tradingSystemOrder, tradingEngineOrder) { + + /* Filter by Session Type */ + switch (bot.SESSION.type) { + case 'Backtesting Session': { + return true + } + case 'Live Trading Session': { + break + } + case 'Fordward Testing Session': { + break + } + case 'Paper Trading Session': { + return true + } + } + + let orderId = await exchangeAPIModule.createOrder(tradingSystemOrder, tradingEngineOrder) + + if (orderId !== undefined) { + tradingEngineOrder.exchangeId.value = orderId + return true + } + } + } + + function simulateExchangeEvents(tradingEngineStage, tradingSystemOrder, tradingEngineOrder) { + + /* Filter by Session Type */ + switch (bot.SESSION.type) { + case 'Backtesting Session': { + break + } + case 'Live Trading Session': { + return + } + case 'Fordward Testing Session': { + return + } + case 'Paper Trading Session': { + break + } + } + + let previousQuotedAssetSize = tradingEngineOrder.orderQuotedAsset.size.value + let previousBaseAssetSizeFilled = tradingEngineOrder.orderBaseAsset.sizeFilled.value + let previousQuotedAssetSizeFilled = tradingEngineOrder.orderQuotedAsset.sizeFilled.value + let previousBaseAssetFeesPaid = tradingEngineOrder.orderBaseAsset.feesPaid.value + let previousQuotedAssetFeesPaid = tradingEngineOrder.orderQuotedAsset.feesPaid.value + + actualRateSimulation() + percentageFilledSimulation() + feesPaidSimulation() + sizeFilledSimulation() + + doTheAccounting( + tradingEngineStage, + tradingSystemOrder, + tradingEngineOrder, + previousBaseAssetSizeFilled, + previousQuotedAssetSizeFilled, + previousBaseAssetFeesPaid, + previousQuotedAssetFeesPaid + ) + + /* If the Stage is Closing and this order is still open, we need to cancel it now */ + if (tradingEngineStage.status.value === 'Closing' && tradingEngineOrder.status.value !== 'Closed') { + simulateCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, 'Closing Stage') + } + + function actualRateSimulation() { + /* Actual Rate Simulation */ + let calculatedBasedOnTradingSystem = false + + basedOnTradingSystem() + basedOnSessionParameters() + considerBestMatchWithOrderBook() + recalculateQuotedAsset() + recalculateSizePlaced() + + function basedOnTradingSystem() { + /* Based on the Trading System Definition */ + if (tradingSystemOrder.simulatedExchangeEvents !== undefined) { + if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate !== undefined) { + if (tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula !== undefined) { + /* Calculate this only once for this order */ + if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.orderStatistics.actualRate.config.initialValue) { + tradingEngineOrder.orderStatistics.actualRate.value = tradingSystem.formulas.get(tradingSystemOrder.simulatedExchangeEvents.simulatedActualRate.formula.id) + if (tradingEngineOrder.orderStatistics.actualRate.value !== undefined) { + calculatedBasedOnTradingSystem = true + } + } + } + } + } + } + + function basedOnSessionParameters() { + /* Based on the Session Parameters Definition */ + if (calculatedBasedOnTradingSystem === false) { + switch (tradingEngineOrder.type) { + case 'Market Order': { + /* Actual Rate is simulated based on the Session Paremeters */ + let slippageAmount = tradingEngineOrder.rate.value * sessionParameters.slippage.config.positionRate / 100 + switch (tradingSystemOrder.type) { + case 'Market Sell Order': { + tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value - slippageAmount + break + } + case 'Market Buy Order': { + tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value + slippageAmount + break + } + } + break + } + case 'Limit Order': { + /* In Limit Orders the actual rate is the rate of the order, there is no slippage */ + tradingEngineOrder.orderStatistics.actualRate.value = tradingEngineOrder.rate.value + break + } + } + } + } + + function considerBestMatchWithOrderBook() { + /* + Until here we have got the rate based on the formula definition or based on session parameters slippage. + THe last check is about watching what happened in the market. Let's remember that the exchange will + fill the order with the best possible matches at it's order book. That means that if the rate + we set for the order was too low (for a sale order) or too hight (for a buy order), the actual rate + should be better than expected. + */ + switch (true) { + case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { + if (tradingEngineOrder.orderStatistics.actualRate.value > tradingEngine.current.episode.candle.max.value) { + tradingEngineOrder.orderStatistics.actualRate.value = tradingEngine.current.episode.candle.max.value + } + break + } + case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': { + if (tradingEngineOrder.orderStatistics.actualRate.value < tradingEngine.current.episode.candle.min.value) { + tradingEngineOrder.orderStatistics.actualRate.value = tradingEngine.current.episode.candle.min.value + } + break + } + } + tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10) + } + + function recalculateQuotedAsset() { + /* + Recalculate the Quouted Asset Size based on the newly calculated Actual Rate. + */ + tradingEngineOrder.orderQuotedAsset.size.value = tradingEngineOrder.orderBaseAsset.size.value * tradingEngineOrder.orderStatistics.actualRate.value + tradingEngineOrder.orderQuotedAsset.size.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.size.value, 10) + } + + function recalculateSizePlaced() { + /* + Recaulculate the Size Placed Based on the newly calculated Quoted Asset Size. + */ + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value - + previousQuotedAssetSize + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value + + tradingEngineOrder.orderQuotedAsset.size.value + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) + } + } + + function percentageFilledSimulation() { + /* + Order Filling Simulation: the way we are going to handle this is the following. + For buy orders we are going to check if the price was below the order rate, and + for sell orders we are going to check if the price was above the order rate. + + If we have a partial filling defined we are going to use it to estimate how much + of the order we will consider filled. + */ + let orderWasHit + switch (tradingSystemOrder.type) { + case 'Limit Buy Order': { + if (tradingEngine.current.episode.candle.min.value <= tradingEngineOrder.rate.value) { + orderWasHit = true + } + break + } + case 'Limit Sell Order': { + if (tradingEngine.current.episode.candle.max.value >= tradingEngineOrder.rate.value) { + orderWasHit = true + } + break + } + case 'Market Buy Order': { + orderWasHit = true + break + } + case 'Market Sell Order': { + orderWasHit = true + break + } + } + if (orderWasHit === true) { + let calculatedBasedOnTradingSystem = false + + /* Based on the Trading System Definition */ + if (tradingSystemOrder.simulatedExchangeEvents !== undefined) { + if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill !== undefined) { + if (tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability !== undefined) { + + /* Percentage Filled */ + let percentageFilled = tradingSystemOrder.simulatedExchangeEvents.simulatedPartialFill.config.fillProbability * 100 + if (tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled > 100) { + percentageFilled = 100 - tradingEngineOrder.orderStatistics.percentageFilled.value + } + tradingEngineOrder.orderStatistics.percentageFilled.value = tradingEngineOrder.orderStatistics.percentageFilled.value + percentageFilled + tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10) + calculatedBasedOnTradingSystem = true + } + } + } + /* + If there is no definition for this at the trading system we will assume orders + are 100% filled every single time. + */ + if (calculatedBasedOnTradingSystem === false) { + /* We will assume that 100% of the order was filled */ + tradingEngineOrder.orderStatistics.percentageFilled.value = 100 + } + /* Check if we need to close the order */ + if (tradingEngineOrder.orderStatistics.percentageFilled.value === 100) { + + /* Close this Order */ + tradingEngineOrder.status.value = 'Closed' + tradingEngineOrder.exitType.value = 'Filled' + + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 + + updateEndsWithCycle(tradingEngineOrder) + tradingSystem.infos.push([tradingSystemOrder.id, 'percentageFilledSimulation -> Closing Order with Exit Type ' + tradingEngineOrder.exitType.value]) + } + } + } + + function feesPaidSimulation() { + /* + The way the Fess Paid are calculated is as follows: From the order size we apply the % of fee, + that would give us the total fees for that order. To that we apply the % filled of the orders, + that give use the final result of the fees paid for the current % filled. + + But before that, we will see if there is an specific definition on how to simulate the fees for + this order, or if there is not, we use the fees session parameters definitions. + */ + + /* Based on the Trading System Definition */ + if (tradingSystemOrder.simulatedExchangeEvents !== undefined) { + if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid !== undefined) { + if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage !== undefined) { + applyFeePercentage(tradingEngineOrder, tradingSystemOrder, tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage) + return + } + } + } + + /* + Another way to simulate the Fees Paid is by using the Session Paremeters configuration for Fees. + */ + switch (tradingEngineOrder.type) { + case 'Market Order': { + applyFeePercentage(tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.taker) + break + } + case 'Limit Order': { + applyFeePercentage(tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.maker) + break + } + } + } + + function sizeFilledSimulation() { + /* Size Filled */ + tradingEngineOrder.orderBaseAsset.sizeFilled.value = + tradingEngineOrder.orderBaseAsset.size.value * + tradingEngineOrder.orderStatistics.percentageFilled.value / 100 + + tradingEngineOrder.orderQuotedAsset.sizeFilled.value = + tradingEngineOrder.orderQuotedAsset.size.value * + tradingEngineOrder.orderStatistics.percentageFilled.value / 100 + + tradingEngineOrder.orderBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10) + tradingEngineOrder.orderQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10) + } + } + + async function checkExchangeEvents(tradingEngineStage, tradingSystemOrder, tradingEngineOrder) { + + /* Filter by Session Type */ + switch (bot.SESSION.type) { + case 'Backtesting Session': { + return true + } + case 'Live Trading Session': { + break + } + case 'Fordward Testing Session': { + break + } + case 'Paper Trading Session': { + return true + } + } + + let order = await exchangeAPIModule.getOrder(tradingSystemOrder, tradingEngineOrder) + + if (order === undefined) { + tradingSystem.warnings.push([tradingSystemOrder.id, 'Could not verify the status of this order at the exchange.']) + return false + } + + const AT_EXCHANGE_STATUS = { + OPEN: 'open', + CLOSED: 'closed', + CANCELLED: 'canceled' + } + + /* Status Checks */ + if (order.remaining === 0 && order.status === AT_EXCHANGE_STATUS.CLOSED) { + /* Close this Order */ + tradingEngineOrder.status.value = 'Closed' + tradingEngineOrder.exitType.value = 'Filled' + + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 + + await updateEndsWithCycle(tradingEngineOrder) + tradingSystem.infos.push([tradingSystemOrder.id, 'checkExchangeEvents -> Closing Order with Exit Type ' + tradingEngineOrder.exitType.value]) + + await syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) + return + } + + if (order.remaining > 0 && order.status === AT_EXCHANGE_STATUS.CLOSED) { + /* Close this Order */ + tradingEngineOrder.status.value = 'Closed' + tradingEngineOrder.exitType.value = 'Closed at the Exchange' + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 + + await updateEndsWithCycle(tradingEngineOrder) + tradingSystem.infos.push([tradingSystemOrder.id, 'checkExchangeEvents -> Closing Order with Exit Type ' + tradingEngineOrder.exitType.value]) + + await syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) + await recalculateStageSize(tradingEngineStage, tradingEngineOrder) + return + } + + if (order.status === AT_EXCHANGE_STATUS.CANCELLED) { + /* Close this Order */ + tradingEngineOrder.status.value = 'Closed' + /* + We must be carefull here not to overide an already defined exitType. It can happen + for instance that the order was cancellerd from the bot, but veryfing the cancellation + was not possible because of a connection to the exchange problem. In that case + the exit type was defined but the order was kept open until the verification could be done. + */ + if (tradingEngineOrder.exitType.value === tradingEngineOrder.exitType.config.initialValue) { + tradingEngineOrder.exitType.value = 'Cancelled at the Exchange' + } + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 + + await updateEndsWithCycle(tradingEngineOrder) + tradingSystem.infos.push([tradingSystemOrder.id, 'checkExchangeEvents -> Closing Order with Exit Type ' + tradingEngineOrder.exitType.value]) + + await syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) + await recalculateStageSize(tradingEngineStage, tradingEngineOrder) + return + } + + /* + If the order happens to be at least partially filled, there is a syncronization work + we need to do, that includes discovering which is the Actual Rate the order is being filled, + the Fees Paid and many other thing we need to account for. + */ + if (order.filled > 0) { + await syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) + } + + /* + Forced Cancellation Check: Here we check if we need to cancel this order because the + stage is closing. + */ + if (tradingEngineStage.status.value === 'Closing' && tradingEngineOrder.status.value !== 'Closed') { + await exchangeCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, 'Closing Stage') + } + + return true + } + + async function syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) { + + let previousBaseAssetSize = tradingEngineOrder.orderBaseAsset.size.value + let previousQuotedAssetSize = tradingEngineOrder.orderQuotedAsset.size.value + let previousBaseAssetSizeFilled = tradingEngineOrder.orderBaseAsset.sizeFilled.value + let previousQuotedAssetSizeFilled = tradingEngineOrder.orderQuotedAsset.sizeFilled.value + let previousBaseAssetFeesPaid = tradingEngineOrder.orderBaseAsset.feesPaid.value + let previousQuotedAssetFeesPaid = tradingEngineOrder.orderQuotedAsset.feesPaid.value + + await actualSizeCalculation() + await actualRateCalculation() + await percentageFilledCalculation() + await feesPaidCalculation() + await sizeFilledCalculation() + + await doTheAccounting( + tradingEngineStage, + tradingSystemOrder, + tradingEngineOrder, + previousBaseAssetSizeFilled, + previousQuotedAssetSizeFilled, + previousBaseAssetFeesPaid, + previousQuotedAssetFeesPaid + ) + + return + + async function actualSizeCalculation() { + /* + When we submit the order to the exchange, we do it specifying the order size + in Base Asset. The exchange might take that size or might change it a little + bit if for example, if it does not accept as many decimals as we are sending. + If we identify the situation in which the size accepted is different than the + size we have accounted for, we need to make several adjustments so that the + accounting syncronizes with reality. + */ + if (tradingEngineOrder.orderBaseAsset.size.value === order.amount) { return } + /* + We will sync the Base Asset size, and recalculate the Quoted Asset size too. + */ + tradingEngineOrder.orderBaseAsset.size.value = order.amount + tradingEngineOrder.orderQuotedAsset.size.value = tradingEngineOrder.orderBaseAsset.size.value * tradingEngineOrder.rate.value + + tradingEngineOrder.orderBaseAsset.size.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.size.value, 10) + tradingEngineOrder.orderQuotedAsset.size.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.size.value, 10) + /* + Finaly we need to unaccount the previous size placed and correctly account + for the the new sizes we now have. + */ + tradingEngineStage.stageBaseAsset.sizePlaced.value = + tradingEngineStage.stageBaseAsset.sizePlaced.value - + previousBaseAssetSize + + tradingEngineStage.stageBaseAsset.sizePlaced.value = + tradingEngineStage.stageBaseAsset.sizePlaced.value + + tradingEngineOrder.orderBaseAsset.size.value + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value - + previousQuotedAssetSize + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value + + tradingEngineOrder.orderQuotedAsset.size.value + + tradingEngineStage.stageBaseAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageBaseAsset.sizePlaced.value, 10) + tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) + + previousBaseAssetSize = tradingEngineOrder.orderBaseAsset.size.value + previousQuotedAssetSize = tradingEngineOrder.orderQuotedAsset.size.value + } + + async function actualRateCalculation() { + /* + Actual Rate Calculation: Let's remember that the exchange may not take the rate + we send it with the order (in Limit orders) and for any reason change it a little bit. + For example we might have sent a rate with too many decimals and the exchange truncate + them. Another thing that might happen is that we send a rate that is matching orders already + at the order book. In this case we would get an actual rate not only different than the one + we sent, but also, better since the trading engine inside the exchange would match out orders + with the first orders best by price. + + In the case of Market Orders we don't even send a rate. For all those reason + we get and store the Actual Rate at which the order was filled. + */ + if (order.average !== undefined) { + /* + We will use the average whenever is available. + */ + tradingEngineOrder.orderStatistics.actualRate.value = order.average + } else { + /* + Otherwise we use the order.price. + */ + tradingEngineOrder.orderStatistics.actualRate.value = order.price + } + + tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10) + + if (tradingEngineOrder.orderStatistics.actualRate.value === tradingEngineOrder.rate.value) { return } + /* + Now we know the Actual Rate at which the order was filled. Since the actual rate + is not the same as the Rate we defined for the order, we need to syncronize + the Order Size for Quoted Asset since it was calculated with the Order Size that we + now know it is not the once really used at the exchange. We will recalculate the + Order Size in Quoted Asset and not in Base Asset, since the Order Size in Base Asset + is the one we sent to the exchange, so that is fixed, regardless of how we initially + got the Order Size in Base Asset, eigher from direct imput from the user or calculated + from the Order Size in Quoted Asset input it by the user. So here we go. + */ + tradingEngineOrder.orderQuotedAsset.size.value = tradingEngineOrder.orderBaseAsset.size.value * tradingEngineOrder.orderStatistics.actualRate.value + tradingEngineOrder.orderQuotedAsset.size.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.size.value, 10) + /* + Great, this up-to-date order size in Base Asset and the Actual Rate can now be used for example to calculate + the orders fees in Quoted Assets, and it is perfectly accurate. The next problem + we need to fix is that the sizePlaced for Quoted Asset that we already have calculated + was based on the Order Rate, not the Actual Rate, so we need to fix that by anuncounting + the previous size and accounting the new one, all this only for Quoted Asset. + */ + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value - + previousQuotedAssetSize + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value + + tradingEngineOrder.orderQuotedAsset.size.value + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) + } + + async function percentageFilledCalculation() { + /* Percentage Filled Calculation */ + tradingEngineOrder.orderStatistics.percentageFilled.value = order.filled * 100 / (order.filled + order.remaining) + tradingEngineOrder.orderStatistics.percentageFilled.value = global.PRECISE(tradingEngineOrder.orderStatistics.percentageFilled.value, 10) + } + + async function feesPaidCalculation() { + /* + The exchange fees are taken from the Base Asset or the Quoted Asset depending if we + are buying or selling. Within the order information received we can not see the fees paid so we need to + calculate them by ourselves. To do this it is needed to be used the Session Fees Parameters. + */ + switch (tradingEngineOrder.type) { + case 'Market Order': { + await applyFeePercentage(tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.taker) + break + } + case 'Limit Order': { + await applyFeePercentage(tradingEngineOrder, tradingSystemOrder, sessionParameters.feeStructure.config.maker) + break + } + } + } + + async function sizeFilledCalculation() { + /* + CCXT returns order.filled with an amount denominated in Base Asset. We will + take it from there for our Order Base Asset. The amount in order.filled does + not account for the fees, which is exactly what we need in this case. + */ + tradingEngineOrder.orderBaseAsset.sizeFilled.value = order.filled + tradingEngineOrder.orderBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10) + /* + For Quoted Asset, CCXT does not return any field with the size filled, so we + need to calculate that by ourselves. Since we know the Size in Quoted Asset thet + is already adjusted with the Actual Rate and the Percentage Filled, we will use all + this in this formula to get the Size Filled in Quoted Asset. + */ + tradingEngineOrder.orderQuotedAsset.sizeFilled.value = + tradingEngineOrder.orderQuotedAsset.size.value * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 + + tradingEngineOrder.orderQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10) + } + } + + async function doTheAccounting( + tradingEngineStage, + tradingSystemOrder, + tradingEngineOrder, + previousBaseAssetSizeFilled, + previousQuotedAssetSizeFilled, + previousBaseAssetFeesPaid, + previousQuotedAssetFeesPaid + ) { + + await updateStageAssets() + await updateBalances() + + async function updateStageAssets() { + /* Stage Base Asset: Undo the previous accounting */ + tradingEngineStage.stageBaseAsset.sizeFilled.value = + tradingEngineStage.stageBaseAsset.sizeFilled.value - + previousBaseAssetSizeFilled + + tradingEngineStage.stageBaseAsset.feesPaid.value = + tradingEngineStage.stageBaseAsset.feesPaid.value - + previousBaseAssetFeesPaid + + /* Stage Base Asset: Account the current filling and fees */ + tradingEngineStage.stageBaseAsset.sizeFilled.value = + tradingEngineStage.stageBaseAsset.sizeFilled.value + + tradingEngineOrder.orderBaseAsset.sizeFilled.value + + tradingEngineStage.stageBaseAsset.feesPaid.value = + tradingEngineStage.stageBaseAsset.feesPaid.value + + tradingEngineOrder.orderBaseAsset.feesPaid.value + + /* Stage Quote Asset: Undo the previous accounting */ + tradingEngineStage.stageQuotedAsset.sizeFilled.value = + tradingEngineStage.stageQuotedAsset.sizeFilled.value - + previousQuotedAssetSizeFilled + + tradingEngineStage.stageQuotedAsset.feesPaid.value = + tradingEngineStage.stageQuotedAsset.feesPaid.value - + previousQuotedAssetFeesPaid + + /* Stage Quote Asset: Account the current filling and fees */ + tradingEngineStage.stageQuotedAsset.sizeFilled.value = + tradingEngineStage.stageQuotedAsset.sizeFilled.value + + tradingEngineOrder.orderQuotedAsset.sizeFilled.value + + tradingEngineStage.stageQuotedAsset.feesPaid.value = + tradingEngineStage.stageQuotedAsset.feesPaid.value + + tradingEngineOrder.orderQuotedAsset.feesPaid.value + + tradingEngineStage.stageBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineStage.stageBaseAsset.sizeFilled.value, 10) + tradingEngineStage.stageBaseAsset.feesPaid.value = global.PRECISE(tradingEngineStage.stageBaseAsset.feesPaid.value, 10) + + tradingEngineStage.stageQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizeFilled.value, 10) + tradingEngineStage.stageQuotedAsset.feesPaid.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.feesPaid.value, 10) + } + + async function updateBalances() { + /* Balances Update */ + switch (true) { + /* + Let's remember that for Buy orders, the fees are paid in Base Asset. So we + actually don't receive the whole Size Filled in Base Asset, but the Size + Filled minus the Fees Paid. For the case of Quoted Asset, we actually + need to susbstract the whole Size Filled from the balance. + */ + case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { + + /* Balance Base Asset: Undo the previous accounting */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value - + previousBaseAssetSizeFilled + + previousBaseAssetFeesPaid + + /* Balance Base Asset: Account the current filling and fees */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value + + tradingEngineOrder.orderBaseAsset.sizeFilled.value - + tradingEngineOrder.orderBaseAsset.feesPaid.value + + /* Balance Quoted Asset: Undo the previous accounting */ + tradingEngine.current.episode.episodeQuotedAsset.balance.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value + + previousQuotedAssetSizeFilled + + /* Balance Quoted Asset: Account the current filling and fees */ + tradingEngine.current.episode.episodeQuotedAsset.balance.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value - + tradingEngineOrder.orderQuotedAsset.sizeFilled.value + break + } + /* + For Sell orders, the fees are being paid in Quote Asset, so we never + acutally receive the whole Filled Size in Quote Asset, but the Filled + Size minus the Fees Paid. But we do expend the whole Filled Size in + Base Asset. + */ + case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': { + + /* Balance Base Asset: Undo the previous accounting */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value + + previousBaseAssetSizeFilled + + /* Balance Base Asset: Account the current filling and fees */ + tradingEngine.current.episode.episodeBaseAsset.balance.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value - + tradingEngineOrder.orderBaseAsset.sizeFilled.value + + /* Balance Quoted Asset: Undo the previous accounting */ + tradingEngine.current.episode.episodeQuotedAsset.balance.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value - + previousQuotedAssetSizeFilled + + previousQuotedAssetFeesPaid + + /* Balance Quoted Asset: Account the current filling and fees */ + tradingEngine.current.episode.episodeQuotedAsset.balance.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value + + tradingEngineOrder.orderQuotedAsset.sizeFilled.value - + tradingEngineOrder.orderQuotedAsset.feesPaid.value + break + } + } + tradingEngine.current.episode.episodeBaseAsset.balance.value = global.PRECISE(tradingEngine.current.episode.episodeBaseAsset.balance.value, 10) + tradingEngine.current.episode.episodeQuotedAsset.balance.value = global.PRECISE(tradingEngine.current.episode.episodeQuotedAsset.balance.value, 10) + } + } + + function simulateCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, exitType) { + + /* Filter by Session Type */ + switch (bot.SESSION.type) { + case 'Backtesting Session': { + break + } + case 'Live Trading Session': { + return + } + case 'Fordward Testing Session': { + return + } + case 'Paper Trading Session': { + break + } + } + + /* Close this Order */ + tradingEngineOrder.status.value = 'Closed' + tradingEngineOrder.exitType.value = exitType + + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 + + updateEndsWithCycle(tradingEngineOrder) + tradingSystem.infos.push([tradingSystemOrder.id, 'simulateCancelOrder -> Closing Order with Exit Type ' + tradingEngineOrder.exitType.value]) + + recalculateStageSize(tradingEngineStage, tradingEngineOrder) + } + + async function exchangeCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, exitType) { + + /* Filter by Session Type */ + switch (bot.SESSION.type) { + case 'Backtesting Session': { + return + } + case 'Live Trading Session': { + break + } + case 'Fordward Testing Session': { + break + } + case 'Paper Trading Session': { + return + } + } + + /* Check if we can cancel the order at the Exchange. */ + let result = await exchangeAPIModule.cancelOrder(tradingSystemOrder, tradingEngineOrder) + if (result === true) { + + /* At this point we know which is the exit type for this order */ + tradingEngineOrder.exitType.value = exitType + + /* + Perhaps the order was filled a bit more between the last time we checked and when it was cancelled. + To sync our accounting, we need to check the order one last time and if it changed, fix it. + */ + + let order = await exchangeAPIModule.getOrder(tradingSystemOrder, tradingEngineOrder) + + if (order === undefined) { + tradingSystem.warnings.push([tradingSystemOrder.id, 'Could not verify the status of this order at the exchange, and syncronize the accounting.']) + return false + } + + /* + Close this Order. Note that we are not closing the order until we have the exchange + response with the order details that we can use to syncronize with our accoounting. + Otherwise if the connection to the exchange fails, we would have a closed order not + accounted in any way. + */ + tradingEngineOrder.status.value = 'Closed' + + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 + + await updateEndsWithCycle(tradingEngineOrder) + tradingSystem.infos.push([tradingSystemOrder.id, 'exchangeCancelOrder -> Closing Order with Exit Type ' + tradingEngineOrder.exitType.value]) + + await syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) + + await recalculateStageSize(tradingEngineStage, tradingEngineOrder) + } + } + + async function recalculateStageSize(tradingEngineStage, tradingEngineOrder) { + /* + Since the order is Cancelled, we need to adjust the stage sizePlaced. Remember that the Stage + Size Placed accumulates for each asset, the order size placed at the exchange. A cancelation means that + only the part filled can be considered placed, so we need to substract from the stage size + the remainder. To achieve this with the information we currently have, we are going first + to unaccount the order size, and the account only the sizeFilled + the feesPaid. + */ + tradingEngineStage.stageBaseAsset.sizePlaced.value = + tradingEngineStage.stageBaseAsset.sizePlaced.value - + tradingEngineOrder.orderBaseAsset.size.value + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value - + tradingEngineOrder.orderQuotedAsset.size.value + + tradingEngineStage.stageBaseAsset.sizePlaced.value = + tradingEngineStage.stageBaseAsset.sizePlaced.value + + tradingEngineOrder.orderBaseAsset.sizeFilled.value + + tradingEngineStage.stageQuotedAsset.sizePlaced.value = + tradingEngineStage.stageQuotedAsset.sizePlaced.value + + tradingEngineOrder.orderQuotedAsset.sizeFilled.value + + tradingEngineStage.stageBaseAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageBaseAsset.sizePlaced.value, 10) + tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) + } + + async function applyFeePercentage(tradingEngineOrder, tradingSystemOrder, feePercentage) { + switch (true) { + case tradingSystemOrder.type === 'Market Buy Order' || tradingSystemOrder.type === 'Limit Buy Order': { + /* + In this case the fees are taken from the Base Asset you receive. + */ + tradingEngineOrder.orderBaseAsset.feesPaid.value = + tradingEngineOrder.orderBaseAsset.size.value * + feePercentage / 100 * + tradingEngineOrder.orderStatistics.percentageFilled.value / 100 + break + } + case tradingSystemOrder.type === 'Market Sell Order' || tradingSystemOrder.type === 'Limit Sell Order': { + /* + In this case the fees are taken from the Quoted Asset you receive. + */ + tradingEngineOrder.orderQuotedAsset.feesPaid.value = + tradingEngineOrder.orderQuotedAsset.size.value * + feePercentage / 100 * + tradingEngineOrder.orderStatistics.percentageFilled.value / 100 + break + } + } + + tradingEngineOrder.orderBaseAsset.feesPaid.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.feesPaid.value, 10) + tradingEngineOrder.orderQuotedAsset.feesPaid.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.feesPaid.value, 10) + } + + async function checkOrderEvent(event, order, executionAlgorithm, executionNode) { + if (event !== undefined) { + for (let k = 0; k < event.situations.length; k++) { + let situation = event.situations[k] + let passed + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(event.id) + tradingSystem.highlights.push(order.id) + tradingSystem.highlights.push(executionAlgorithm.id) + tradingSystem.highlights.push(executionNode.id) + + announcementsModule.makeAnnoucements(event) + return situation.name // if the event is triggered, we return the name of the situation that passed + } + } + } + } + + function updateEnds(tradingEngineOrder) { + if (tradingEngineOrder.status.value === 'Open') { + tradingEngineOrder.end.value = tradingEngine.current.episode.candle.end.value + } + } + + async function updateEndsWithCycle(tradingEngineOrder) { + tradingEngineOrder.end.value = tradingEngine.current.episode.cycle.end.value + } + + function updateCounters(tradingEngineOrder) { + if (tradingEngineOrder.status.value === 'Open') { + tradingEngineOrder.orderCounters.periods.value++ + } + } + + function updateStatistics(tradingEngineOrder) { + tradingEngineOrder.orderStatistics.days.value = tradingEngineOrder.orderCounters.periods.value * sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS + tradingEngineOrder.orderStatistics.days.value = global.PRECISE(tradingEngineOrder.orderStatistics.days.value, 10) + } + + function resetTradingEngineDataStructure(tradingEngineOrder, tradingSystemOrder, stageStatus) { + if (tradingEngineOrder.status.value === 'Closed') { + /* We reset the order data structure inside the Trading Engine to its initial value */ + tradingEngineModule.initializeNode(tradingEngineOrder) + if (tradingSystemOrder.config.spawnMultipleOrders !== true) { + /* + We close the lock so as to prevent this data structure to be used again during this same stage execution. + */ + if (stageStatus === 'Open') { + tradingEngineOrder.lock.value = 'Closed' + } + } + } + } + + function badDefinitionUnhandledException(err, message, node) { + tradingSystem.errors.push([node.id, message]) + + logger.write(MODULE_NAME, "[ERROR] -> " + message); + logger.write(MODULE_NAME, "[ERROR] -> node.name = " + node.name); + logger.write(MODULE_NAME, "[ERROR] -> node.type = " + node.type); + logger.write(MODULE_NAME, "[ERROR] -> node.config = " + JSON.stringify(node.config)); + if (err !== undefined) { + logger.write(MODULE_NAME, "[ERROR] -> err.stack = " + err.stack); + } + throw 'Please fix the problem and try again.' + } +} + diff --git a/LowFrequencyTrading/TradingOutput.js b/LowFrequencyTrading/TradingOutput.js index 0abe8c40a..1f582d090 100644 --- a/LowFrequencyTrading/TradingOutput.js +++ b/LowFrequencyTrading/TradingOutput.js @@ -1,9 +1,8 @@ -exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FILE_STORAGE) { +exports.newTradingOutput = function newTradingOutput(bot, logger, tradingEngineModule, UTILITIES, FILE_STORAGE) { /* This module will load if necesary all the data outputs so that they can be appended with new records if needed. After running the simulation, it will save all the data outputs. */ - const FULL_LOG = true const MODULE_NAME = 'Trading Bot' let thisObject = { @@ -24,21 +23,13 @@ exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FIL commons = undefined } - function initialize(callBackFunction) { - try { - logger.fileName = MODULE_NAME - logger.initialize() + function initialize() { - callBackFunction(global.DEFAULT_OK_RESPONSE) - } catch (err) { - logger.write(MODULE_NAME, '[ERROR] initialize -> err = ' + err.stack) - callBackFunction(global.DEFAULT_FAIL_RESPONSE) - } } - function start(chart, timeFrame, timeFrameLabel, currentDay, callBackFunction) { + async function start(chart, timeFrame, timeFrameLabel, tradingProcessDate) { try { - bot.processingDailyFiles + if (timeFrame > global.dailyFilePeriods[0][0]) { bot.processingDailyFiles = false } else { @@ -47,37 +38,59 @@ exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FIL /* Preparing everything for the Simulation */ const TRADING_SIMULATION = require('./TradingSimulation.js') - let tradingSimulation = TRADING_SIMULATION.newTradingSimulation(bot, logger, UTILITIES) + let tradingSimulation = TRADING_SIMULATION.newTradingSimulation(bot, logger, tradingEngineModule, UTILITIES) - let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets - let market = bot.market - let totalFilesToBeRead = 0 - let totalFilesRead = 0 - let totalFilesToBeWritten = 0 - let totalFilesWritten = 0 + let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset') let outputDatasetsMap = new Map() - if (bot.FIRST_EXECUTION === true && bot.RESUME === false) { - /* - Here is where the Trading Engine and Trading Systems received are moved to the simulation state. + if (bot.processingDailyFiles === true) { + /* + For Daily Files, we are going to initialize the outputs at the first execution + if we are not resuming, but also at every following execution except when + we are at the head of the market, because at the head of the market we need to + append more data to the same output files that already exists. */ - bot.simulationState.tradingEngine = bot.TRADING_ENGINE - bot.simulationState.tradingSystem = bot.TRADING_SYSTEM - initializeOutputs() - } else { - readFiles() - } - return - - function initializeOutputs() { - if (bot.processingDailyFiles) { - initializeDailyFiles() + if (bot.FIRST_EXECUTION === true && bot.RESUME === false) { + await initializeOutputs() } else { - initializeMarketFiles() + if (bot.simulationState.tradingEngine.current.episode.headOfTheMarket.value === true) { + await readFiles() + } else { + await initializeOutputs() + } + } + } else { + /* + Form Market Files, we are only going to initialize the outputs at the first run + and if we are not resuming execution. Subsequent runs means that we are at the + head of the market. Remember that backtests finishes the session once the reach + the final datetime. + */ + if (bot.FIRST_EXECUTION === true && bot.RESUME === false) { + await initializeOutputs() + } else { + await readFiles() } } - function initializeDailyFiles() { + await tradingSimulation.runSimulation( + chart, + outputDatasetsMap, + writeFiles + ) + + tradingSimulation.finalize() + return + + async function initializeOutputs() { + if (bot.processingDailyFiles) { + await initializeDailyFiles() + } else { + await initializeMarketFiles() + } + } + + async function initializeDailyFiles() { for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent @@ -86,10 +99,9 @@ exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FIL outputDatasetsMap.set(dataset.parentNode.config.codeName, []) } } - runSimulation() } - function initializeMarketFiles() { + async function initializeMarketFiles() { for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent @@ -98,23 +110,22 @@ exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FIL outputDatasetsMap.set(dataset.parentNode.config.codeName, []) } } - runSimulation() } - function readFiles() { + async function readFiles() { /* This bot have an output of files that it generates. At every call to the bot, it needs to read the previously generated files in order to later append more information after the execution is over. Here in this function we are going to read those output files and get them ready for appending content during the simulation. */ if (bot.processingDailyFiles) { - readDailyFiles() + await readDailyFiles() } else { - readMarketFiles() + await readMarketFiles() } } - function readMarketFiles() { + async function readMarketFiles() { for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent @@ -124,86 +135,54 @@ exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FIL let fileName = 'Data.json' let filePath = bot.filePathRoot + '/Output/' + bot.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel - readOutputFile(fileName, filePath, dataset.parentNode.config.codeName) + await readOutputFile(fileName, filePath, dataset.parentNode.config.codeName) } } } - function readDailyFiles() { + async function readDailyFiles() { for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent if (dataset.config.type === 'Daily Files') { - let dateForPath = currentDay.getUTCFullYear() + '/' + utilities.pad(currentDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(currentDay.getUTCDate(), 2); + let dateForPath = tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(tradingProcessDate.getUTCDate(), 2); let fileName = 'Data.json' let filePath = bot.filePathRoot + '/Output/' + bot.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel + "/" + dateForPath - readOutputFile(fileName, filePath, dataset.parentNode.config.codeName) + await readOutputFile(fileName, filePath, dataset.parentNode.config.codeName) } } } - function readOutputFile(fileName, filePath, productName) { - totalFilesToBeRead++ + async function readOutputFile(fileName, filePath, productName) { filePath += '/' + fileName - fileStorage.getTextFile(filePath, onFileRead, true) - function onFileRead(err, text) { - try { + let response = await fileStorage.asyncGetTextFile(filePath, true) - if (err.message === 'File does not exist.') { - outputDatasetsMap.set(productName, []) - anotherFileRead() - return - } - - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack) - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> filePath = ' + filePath) - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> market = ' + market.baseAsset + '_' + market.quotedAsset) - - callBackFunction(err) - return - } - - outputDatasetsMap.set(productName, JSON.parse(text)) - anotherFileRead() - } catch (err) { - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack) - callBackFunction(global.DEFAULT_FAIL_RESPONSE) - } + if (response.err.message === 'File does not exist.') { + outputDatasetsMap.set(productName, []) + return } - } - - function anotherFileRead() { - totalFilesRead++ - if (totalFilesRead === totalFilesToBeRead) { - runSimulation() + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw(response.err) } + outputDatasetsMap.set(productName, JSON.parse(response.text)) } - function runSimulation() { - tradingSimulation.runSimulation( - chart, - outputDatasetsMap, - writeFiles, - callBackFunction) - } - - function writeFiles() { + async function writeFiles() { /* The output of files which were appended with information during the simulation execution, now needs to be saved. */ if (bot.processingDailyFiles) { - writeDailyFiles() + await writeDailyFiles() } else { - writeMarketFiles() + await writeMarketFiles() } } - function writeMarketFiles() { + async function writeMarketFiles() { for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent @@ -213,63 +192,41 @@ exports.newTradingOutput = function newTradingOutput(bot, logger, UTILITIES, FIL let fileName = 'Data.json' let filePath = bot.filePathRoot + '/Output/' + bot.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel - writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName) + await writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName) } } } - function writeDailyFiles() { + async function writeDailyFiles() { for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent if (dataset.config.type === 'Daily Files') { - let dateForPath = currentDay.getUTCFullYear() + '/' + utilities.pad(currentDay.getUTCMonth() + 1, 2) + '/' + utilities.pad(currentDay.getUTCDate(), 2); + let dateForPath = tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(tradingProcessDate.getUTCDate(), 2); let fileName = 'Data.json' let filePath = bot.filePathRoot + '/Output/' + bot.SESSION.folderName + '/' + dataset.parentNode.config.codeName + '/' + dataset.config.codeName + '/' + timeFrameLabel + "/" + dateForPath - writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName) + await writeOutputFile(fileName, filePath, dataset.parentNode.config.codeName) } } } - function writeOutputFile(fileName, filePath, productName) { - totalFilesToBeWritten++ + async function writeOutputFile(fileName, filePath, productName) { filePath += '/' + fileName let fileContent = JSON.stringify(outputDatasetsMap.get(productName)) - fileStorage.createTextFile(filePath, fileContent, onFileCreated) + let response = await fileStorage.asyncCreateTextFile(filePath, fileContent) - function onFileCreated(err) { - try { - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack) - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> filePath = ' + filePath) - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> market = ' + market.baseAsset + '_' + market.quotedAsset) - - callBackFunction(err) - return - } - - anotherFileWritten() - } catch (err) { - logger.write(MODULE_NAME, '[ERROR] start -> readMarketFiles -> readOutputFile -> onFileRead -> err = ' + err.stack) - callBackFunction(global.DEFAULT_FAIL_RESPONSE) - } - } - } - - function anotherFileWritten() { - totalFilesWritten++ - if (totalFilesWritten === totalFilesToBeWritten) { - callBackFunction(global.DEFAULT_OK_RESPONSE) + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw(response.err) } } } catch (err) { logger.write(MODULE_NAME, '[ERROR] start -> err = ' + err.stack) - callBackFunction(global.DEFAULT_FAIL_RESPONSE) + throw(global.DEFAULT_FAIL_RESPONSE) } } } diff --git a/LowFrequencyTrading/TradingPosition.js b/LowFrequencyTrading/TradingPosition.js index 8e38d100d..8476a4685 100644 --- a/LowFrequencyTrading/TradingPosition.js +++ b/LowFrequencyTrading/TradingPosition.js @@ -4,55 +4,76 @@ exports.newTradingPosition = function newTradingPosition(bot, logger, tradingEng */ const MODULE_NAME = 'Trading Position' let thisObject = { + mantain: mantain, + reset: reset, + cycleBasedStatistics: cycleBasedStatistics, openPosition: openPosition, closingPosition: closingPosition, closePosition: closePosition, applyStopLossFormula: applyStopLossFormula, applyTakeProfitFormula: applyTakeProfitFormula, - preventStopLossDistortion: preventStopLossDistortion, - applySlippageToStopLoss: applySlippageToStopLoss, - preventTakeProfitDistortion: preventTakeProfitDistortion, - applySlippageToTakeProfit: applySlippageToTakeProfit, updateStopLoss: updateStopLoss, updateTakeProfit: updateTakeProfit, - updateSizeAndRate: updateSizeAndRate, + initialTargets: initialTargets, updateEnds: updateEnds, - updateStatus: updateStatus, + resetTradingEngineDataStructure: resetTradingEngineDataStructure, updateCounters: updateCounters, - updateStatistics: updateStatistics, - resetPosition: resetPosition, initialize: initialize, finalize: finalize } let tradingEngine + let tradingSystem let sessionParameters return thisObject function initialize() { + tradingSystem = bot.simulationState.tradingSystem tradingEngine = bot.simulationState.tradingEngine sessionParameters = bot.SESSION.parameters } function finalize() { tradingEngine = undefined + tradingSystem = undefined sessionParameters = undefined } - function openPosition(situationName) { - tradingEngine.current.position.status.value = 'Open' - tradingEngine.current.position.serialNumber.value = tradingEngine.episode.episodeCounters.positions.value - tradingEngine.current.position.identifier.value = global.UNIQUE_ID() - tradingEngine.current.position.begin.value = tradingEngine.current.candle.begin.value - tradingEngine.current.position.end.value = tradingEngine.current.candle.end.value + function mantain() { + updateCounters() + updateEnds() + } + function reset() { + resetTradingEngineDataStructure() + } + + function openPosition(situationName) { + + /* Recording the opening at the Trading Engine Data Structure */ + tradingEngine.current.position.status.value = 'Open' + tradingEngine.current.position.serialNumber.value = tradingEngine.current.episode.episodeCounters.positions.value + 1 + tradingEngine.current.position.identifier.value = global.UNIQUE_ID() + tradingEngine.current.position.begin.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.position.beginRate.value = tradingEngine.current.episode.candle.close.value + tradingEngine.current.position.positionBaseAsset.beginBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngine.current.position.positionQuotedAsset.beginBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value tradingEngine.current.position.situationName.value = situationName - tradingEngine.current.position.stopLoss.stopLossStage.value = 'Open Stage' - tradingEngine.current.position.takeProfit.takeProfitStage.value = 'Open Stage' - tradingEngine.current.position.stopLoss.stopLossPhase.value = 0 - tradingEngine.current.position.takeProfit.takeProfitPhase.value = 0 + /* Initializing Stop and Take Phase */ + tradingEngine.current.position.stopLoss.stopLossPhase.value = 1 + tradingEngine.current.position.takeProfit.takeProfitPhase.value = 1 + + /* Updating Episode Counters */ + tradingEngine.current.episode.episodeCounters.positions.value++ + + /* Inicializing this counter */ + tradingEngine.current.episode.distanceToEvent.takePosition.value = 1 + + /* Remember the balance we had before taking the position to later calculate profit or loss */ + tradingEngine.current.position.positionBaseAsset.beginBalance = tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngine.current.position.positionQuotedAsset.beginBalance = tradingEngine.current.episode.episodeQuotedAsset.balance.value } function closingPosition(exitType) { @@ -62,15 +83,31 @@ exports.newTradingPosition = function newTradingPosition(bot, logger, tradingEng function closePosition() { tradingEngine.current.position.status.value = 'Closed' - tradingEngine.current.position.end.value = tradingEngine.current.candle.end.value + tradingEngine.current.position.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.position.endRate.value = tradingEngine.current.episode.candle.close.value + tradingEngine.current.position.positionBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngine.current.position.positionQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value + /* Now that the position is closed, it is the right time to move this position from current to last at the Trading Engine data structure. */ tradingEngineModule.cloneValues(tradingEngine.current.position, tradingEngine.last.position) - } - function resetPosition() { - tradingEngine.current.position.initialize(tradingEngine.current.position) + cycleBasedStatistics() + + /* Updating Hits & Fails */ + if (tradingEngine.current.position.positionBaseAsset.profitLoss.value > 0) { + tradingEngine.current.episode.episodeBaseAsset.hits.value++ + } + if (tradingEngine.current.position.positionBaseAsset.profitLoss.value < 0) { + tradingEngine.current.episode.episodeBaseAsset.fails.value++ + } + if (tradingEngine.current.position.positionQuotedAsset.profitLoss.value > 0) { + tradingEngine.current.episode.episodeQuotedAsset.hits.value++ + } + if (tradingEngine.current.position.positionQuotedAsset.profitLoss.value < 0) { + tradingEngine.current.episode.episodeQuotedAsset.fails.value++ + } } function applyStopLossFormula(formulas, formulaId) { @@ -81,97 +118,151 @@ exports.newTradingPosition = function newTradingPosition(bot, logger, tradingEng tradingEngine.current.position.takeProfit.value = formulas.get(formulaId) } - function updateStopLoss(phase, stage) { + function updateStopLoss(phase) { tradingEngine.current.position.stopLoss.stopLossPhase.value = phase - tradingEngine.current.position.stopLoss.stopLossStage.value = stage } - function updateTakeProfit(phase, stage) { + function updateTakeProfit(phase) { tradingEngine.current.position.takeProfit.takeProfitPhase.value = phase - tradingEngine.current.position.takeProfit.takeProfitStage.value = stage } - function updateSizeAndRate(size, rate) { - tradingEngine.current.position.size.value = global.PRECISE(size, 10) - tradingEngine.current.position.rate.value = global.PRECISE(rate, 10) + function initialTargets(tradingSystemStageNode, tradingEngineStageNode) { - tradingEngine.current.position.beginRate.value = tradingEngine.current.position.rate.value - } - - function preventStopLossDistortion() { - /* - 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.sessionAndMarketBaseAssetsAreEqual) { - if (tradingEngine.current.position.stopLoss.value < tradingEngine.current.candle.min.value) { - tradingEngine.current.position.stopLoss.value = tradingEngine.current.candle.min.value - } - } else { - if (tradingEngine.current.position.stopLoss.value > tradingEngine.current.candle.max.value) { - tradingEngine.current.position.stopLoss.value = tradingEngine.current.candle.max.value - } - } - } - - function applySlippageToStopLoss() { - let slippedStopLoss = tradingEngine.current.position.stopLoss.value - - /* Apply the Slippage */ - let slippageAmount = slippedStopLoss * bot.SESSION.parameters.slippage.config.stopLoss / 100 - - if (bot.sessionAndMarketBaseAssetsAreEqual) { - slippedStopLoss = slippedStopLoss + slippageAmount - } else { - slippedStopLoss = slippedStopLoss - slippageAmount + if (tradingSystemStageNode.initialTargets === undefined) { + const message = 'Stage without Initial Targets node. Add one please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode) } - tradingEngine.current.position.endRate.value = slippedStopLoss - } + setTargetRate() + setTargetSize() - function preventTakeProfitDistortion() { - /* - 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.sessionAndMarketBaseAssetsAreEqual) { - if (tradingEngine.current.position.takeProfit.value > tradingEngine.current.candle.max.value) { - tradingEngine.current.position.takeProfit.value = tradingEngine.current.candle.max.value + function setTargetRate() { + if (tradingSystemStageNode.initialTargets.targetRate === undefined) { + const message = 'Target Rate Node not Found. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets) } - } else { - if (tradingEngine.current.position.takeProfit.value < tradingEngine.current.candle.min.value) { - tradingEngine.current.position.takeProfit.value = tradingEngine.current.candle.min.value + if (tradingSystemStageNode.initialTargets.targetRate.formula === undefined) { + const message = 'Formula of Target Rate Node not Found. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate) + } + + let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetRate.formula.id) + if (value === undefined) { + const message = 'Target Rate can not be undefined. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate.formula) + } + if (isNaN(value)) { + const message = 'Target Rate must be a number. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetRate.formula) + } + + switch (tradingSystemStageNode.type) { + case 'Open Stage': { + tradingEngine.current.position.entryTargetRate.value = global.PRECISE(value, 10) + break + } + case 'Close Stage': { + tradingEngine.current.position.exitTargetRate.value = global.PRECISE(value, 10) + break + } } } - } - function applySlippageToTakeProfit() { - let slippedTakeProfit = tradingEngine.current.position.takeProfit.value - /* Apply the Slippage */ - let slippageAmount = slippedTakeProfit * bot.SESSION.parameters.slippage.config.takeProfit / 100 + function setTargetSize() { + /* Basic Validation */ + if ( + tradingSystemStageNode.initialTargets.targetSizeInBaseAsset !== undefined && + tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset !== undefined + ) { + const message = 'Only Target Size In Base Asset or Target Size In Quoted Asset is allowed. Remove one of them please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode) + } - if (bot.sessionAndMarketBaseAssetsAreEqual) { - slippedTakeProfit = slippedTakeProfit + slippageAmount - } else { - slippedTakeProfit = slippedTakeProfit - slippageAmount + /* Position In Base Asset */ + if (tradingSystemStageNode.initialTargets.targetSizeInBaseAsset !== undefined) { + if (tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula !== undefined) { + let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula.id) + if (value === undefined) { + const message = 'Target Size In Base Asset cannot be undefined. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula) + } + if (value === undefined) { + const message = 'Target Size In Base Asset cannot be zero. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset.formula) + } + + switch (tradingSystemStageNode.type) { + case 'Open Stage': { + tradingEngine.current.position.positionBaseAsset.entryTargetSize.value = global.PRECISE(value, 10) + tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value = + global.PRECISE(value * tradingEngine.current.position.entryTargetRate.value, 10) + break + } + case 'Close Stage': { + tradingEngine.current.position.positionBaseAsset.exitTargetSize.value = global.PRECISE(value, 10) + tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value = + global.PRECISE(value * tradingEngine.current.position.exitTargetRate.value, 10) + break + } + } + + /* Remember how the end user defined this stage. */ + tradingEngineStageNode.stageDefinedIn.value = 'Base Asset' + } else { + const message = 'You need to specify a Formula for this.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInBaseAsset) + } + } + + /* Position In Quoted Asset */ + if (tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset !== undefined) { + if (tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula !== undefined) { + let value = tradingSystem.formulas.get(tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula.id) + if (value === undefined) { + const message = 'Target Size In Quoted Asset cannot be undefined. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula) + } + if (value === undefined) { + const message = 'Target Size In Quoted Asset cannot be zero. Fix this please.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula) + } + switch (tradingSystemStageNode.type) { + case 'Open Stage': { + tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value = global.PRECISE(value, 10) + tradingEngine.current.position.positionBaseAsset.entryTargetSize.value = + global.PRECISE(value / tradingEngine.current.position.entryTargetRate.value, 10) + break + } + case 'Close Stage': { + tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value = global.PRECISE(value, 10) + tradingEngine.current.position.positionBaseAsset.exitTargetSize.value = + global.PRECISE(value / tradingEngine.current.position.exitTargetRate.value, 10) + break + } + } + + /* Remember how the end user defined this stage. */ + tradingEngineStageNode.stageDefinedIn.value = 'Quoted Asset' + } else { + const errorText = 'You need to specify a Formula for this.' + badDefinitionUnhandledException(undefined, message, tradingSystemStageNode.initialTargets.targetSizeInQuotedAsset.formula) + } + } } - - tradingEngine.current.position.endRate.value = slippedTakeProfit } function updateEnds() { if (tradingEngine.current.position.status.value === 'Open') { - tradingEngine.current.position.end.value = tradingEngine.current.candle.end.value + tradingEngine.current.position.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.position.endRate.value = tradingEngine.current.episode.candle.close.value + tradingEngine.current.position.positionBaseAsset.endBalance.value = tradingEngine.current.episode.episodeBaseAsset.balance.value + tradingEngine.current.position.positionQuotedAsset.endBalance.value = tradingEngine.current.episode.episodeQuotedAsset.balance.value } } - function updateStatus() { + function resetTradingEngineDataStructure() { if (tradingEngine.current.position.status.value === 'Closed') { - resetPosition() + tradingEngineModule.initializeNode(tradingEngine.current.position) } } @@ -181,19 +272,109 @@ exports.newTradingPosition = function newTradingPosition(bot, logger, tradingEng } } - function updateStatistics() { - if (bot.sessionAndMarketBaseAssetsAreEqual) { - tradingEngine.current.position.positionStatistics.profitLoss.value = tradingEngine.current.balance.baseAsset.value - tradingEngine.previous.balance.baseAsset.value - tradingEngine.current.position.positionStatistics.ROI.value = tradingEngine.current.position.positionStatistics.profitLoss.value * 100 / tradingEngine.current.position.size.value - } else { - tradingEngine.current.position.positionStatistics.profitLoss.value = tradingEngine.current.balance.quotedAsset.value - tradingEngine.previous.balance.quotedAsset.value - tradingEngine.current.position.positionStatistics.ROI.value = tradingEngine.current.position.positionStatistics.profitLoss.value * 100 / tradingEngine.current.position.size.value + function cycleBasedStatistics() { + + calculateAssetsStatistics() + calculatePositionStatistics() + + function calculateAssetsStatistics() { + /* Profit Loss Calculation */ + tradingEngine.current.position.positionBaseAsset.profitLoss.value = + tradingEngine.current.episode.episodeBaseAsset.balance.value - + tradingEngine.current.position.positionBaseAsset.beginBalance + + tradingEngine.current.position.positionQuotedAsset.profitLoss.value = + tradingEngine.current.episode.episodeQuotedAsset.balance.value - + tradingEngine.current.position.positionQuotedAsset.beginBalance + + tradingEngine.current.position.positionBaseAsset.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionBaseAsset.profitLoss.value, 10) + tradingEngine.current.position.positionQuotedAsset.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionQuotedAsset.profitLoss.value, 10) + + /* ROI Calculation */ + tradingEngine.current.position.positionBaseAsset.ROI.value = + tradingEngine.current.position.positionBaseAsset.profitLoss.value * 100 / + tradingEngine.current.strategyOpenStage.stageBaseAsset.targetSize.value + + tradingEngine.current.position.positionQuotedAsset.ROI.value = + tradingEngine.current.position.positionQuotedAsset.profitLoss.value * 100 / + tradingEngine.current.strategyOpenStage.stageQuotedAsset.targetSize.value + + tradingEngine.current.position.positionBaseAsset.ROI.value = global.PRECISE(tradingEngine.current.position.positionBaseAsset.ROI.value, 10) + tradingEngine.current.position.positionQuotedAsset.ROI.value = global.PRECISE(tradingEngine.current.position.positionQuotedAsset.ROI.value, 10) + + /* Hit Fail Calculation */ + if (tradingEngine.current.position.positionBaseAsset.ROI.value > 0) { + tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Hit' + } + if (tradingEngine.current.position.positionBaseAsset.ROI.value < 0) { + tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Fail' + } + if (tradingEngine.current.position.positionBaseAsset.ROI.value === 0) { + tradingEngine.current.position.positionBaseAsset.hitFail.value = 'Even' + } + if (tradingEngine.current.position.positionQuotedAsset.ROI.value > 0) { + tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Hit' + } + if (tradingEngine.current.position.positionQuotedAsset.ROI.value < 0) { + tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Fail' + } + if (tradingEngine.current.position.positionQuotedAsset.ROI.value === 0) { + tradingEngine.current.position.positionQuotedAsset.hitFail.value = 'Even' + } } - tradingEngine.current.position.positionStatistics.days.value = tradingEngine.current.position.positionCounters.periods.value * sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS - if (tradingEngine.current.position.positionStatistics.ROI.value > 0) { - tradingEngine.current.position.positionStatistics.hitFail.value = 'HIT' - } else { - tradingEngine.current.position.positionStatistics.hitFail.value = 'FAIL' + + function calculatePositionStatistics() { + /* Profit Loss Calculation */ + tradingEngine.current.position.positionStatistics.profitLoss.value = + ( + tradingEngine.current.episode.episodeBaseAsset.profitLoss.value * tradingEngine.current.position.endRate.value + + tradingEngine.current.episode.episodeQuotedAsset.profitLoss.value + ) + + tradingEngine.current.position.positionStatistics.profitLoss.value = global.PRECISE(tradingEngine.current.position.positionStatistics.profitLoss.value, 10) + + /* ROI Calculation */ + tradingEngine.current.position.positionStatistics.ROI.value = + ( + tradingEngine.current.position.positionStatistics.profitLoss.value + ) + * 100 / + ( + tradingEngine.current.position.positionBaseAsset.beginBalance * tradingEngine.current.position.beginRate.value + + tradingEngine.current.position.positionQuotedAsset.beginBalance + ) + tradingEngine.current.position.positionStatistics.ROI.value = global.PRECISE(tradingEngine.current.position.positionStatistics.ROI.value, 10) + + /* Hit Fail Calculation */ + if (tradingEngine.current.position.positionStatistics.ROI.value > 0) { + tradingEngine.current.position.positionStatistics.hitFail.value = 'Hit' + } + if (tradingEngine.current.position.positionStatistics.ROI.value < 0) { + tradingEngine.current.position.positionStatistics.hitFail.value = 'Fail' + } + if (tradingEngine.current.position.positionStatistics.ROI.value === 0) { + tradingEngine.current.position.positionStatistics.hitFail.value = 'Even' + } + + /* Days Calculation */ + tradingEngine.current.position.positionStatistics.days.value = + tradingEngine.current.position.positionCounters.periods.value * + sessionParameters.timeFrame.config.value / global.ONE_DAY_IN_MILISECONDS + + tradingEngine.current.position.positionStatistics.days.value = global.PRECISE(tradingEngine.current.position.positionStatistics.days.value, 10) } } + + function badDefinitionUnhandledException(err, message, node) { + tradingSystem.errors.push([node.id, message]) + + logger.write(MODULE_NAME, "[ERROR] -> " + message); + logger.write(MODULE_NAME, "[ERROR] -> node.name = " + node.name); + logger.write(MODULE_NAME, "[ERROR] -> node.type = " + node.type); + logger.write(MODULE_NAME, "[ERROR] -> node.config = " + JSON.stringify(node.config)); + if (err !== undefined) { + logger.write(MODULE_NAME, "[ERROR] -> err.stack = " + err.stack); + } + throw 'Please fix the problem and try again.' + } } \ No newline at end of file diff --git a/LowFrequencyTrading/TradingProcess.js b/LowFrequencyTrading/TradingProcess.js index eb23e6ae6..bddb9883a 100644 --- a/LowFrequencyTrading/TradingProcess.js +++ b/LowFrequencyTrading/TradingProcess.js @@ -1,10 +1,9 @@ -exports.newTradingProcess = function newTradingProcess(bot, logger, UTILITIES, TRADING_OUTPUT_MODULE, USER_BOT_COMMONS) { +exports.newTradingProcess = function newTradingProcess(bot, logger, UTILITIES) { /* This Module will load all the process data dependencies from files and send them downstream. After execution, will save the time range and status report of the process. */ const FULL_LOG = true; - const LOG_FILE_CONTENT = false; const MODULE_NAME = "Trading Process"; const GMT_SECONDS = ':00.000 GMT+0000'; @@ -21,27 +20,31 @@ let dataFiles = new Map(); let multiPeriodDataFiles = new Map(); - let tradingOutputModule; - const FILE_STORAGE = require('../FileStorage.js'); let fileStorage = FILE_STORAGE.newFileStorage(logger); + const TRADING_ENGINE_MODULE = require('./TradingEngine.js') + let tradingEngineModule = TRADING_ENGINE_MODULE.newTradingEngine(bot, logger) + + let TRADING_OUTPUT_MODULE = require("./TradingOutput") + let tradingOutputModule = TRADING_OUTPUT_MODULE.newTradingOutput(bot, logger, tradingEngineModule, UTILITIES, FILE_STORAGE) + let processConfig; return thisObject; function initialize(pProcessConfig, pStatusDependencies, pDataDependencies, callBackFunction) { try { - logger.fileName = MODULE_NAME; - logger.initialize(); + logger.fileName = MODULE_NAME + logger.initialize() - statusDependencies = pStatusDependencies; - dataDependenciesModule = pDataDependencies; - processConfig = pProcessConfig; + statusDependencies = pStatusDependencies + dataDependenciesModule = pDataDependencies + processConfig = pProcessConfig - let TRADING_OUTPUT_MODULE = require("./TradingOutput") - tradingOutputModule = TRADING_OUTPUT_MODULE.newTradingOutput(bot, logger, UTILITIES, FILE_STORAGE); - tradingOutputModule.initialize(callBackFunction); + tradingOutputModule.initialize() + + callBackFunction(global.DEFAULT_OK_RESPONSE) } catch (err) { logger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack); @@ -50,59 +53,205 @@ } function finalize() { + + tradingEngineModule.finalize() + tradingEngineModule = undefined + + tradingOutputModule.finalize() + tradingOutputModule = undefined + dataFiles = undefined multiPeriodDataFiles = undefined statusDependencies = undefined dataDependenciesModule = undefined - tradingOutputModule.finalize() - tradingOutputModule = undefined fileStorage = undefined processConfig = undefined thisObject = undefined } - function start(callBackFunction) { + async function start(callBackFunction) { try { let currentTimeFrame let currentTimeFrameLabel - let market = bot.market; /* Context Variables */ - let contextVariables = { lastFile: undefined, // Datetime of the last file files sucessfully produced by this process. dateBeginOfMarket: undefined, // Datetime of the first trade file in the whole market history. dateEndOfMarket: undefined // Datetime of the last file available to be used as an input of this process. }; - let previousDay; // Holds the date of the previous day relative to the processing date. + getContextVariables() - getContextVariables(); + if (bot.FIRST_EXECUTION === true && bot.RESUME === false) { + /* + Here is where the Trading Engine and Trading Systems received are moved to the simulation state. + */ + bot.simulationState.tradingEngine = bot.TRADING_ENGINE + bot.simulationState.tradingSystem = bot.TRADING_SYSTEM + } + + /* We set up the Trading Engine Module. */ + tradingEngineModule.initialize() + + /* Initializing the Trading Process Date */ + if (bot.FIRST_EXECUTION === true && bot.RESUME === false) { + /* + This funcion is going to be called many times by the Trading Bot Loop. + Only during the first execution and when the User is not resuming the execution + of a stopped session / task; we are going to initialize the Process Date Time. + This variable tell us which day we are standing at, specially while working + with Daily Files. From this Date is that we are going to load the Daily Files. + */ + bot.simulationState.tradingEngine.current.episode.processDate.value = global.REMOVE_TIME(bot.SESSION.parameters.timeRange.config.initialDatetime).valueOf() + } + + /* + This is the Date that is going to be used across the execution of this Trading Process. + We need this because it has a different life cycle than the processData stored at the + Trading Engine data structure. This date has to remain the same during the whole execution + of the Trading Process until the end, inclusind the writting of Data Ranges and Status Reports. + The processDate of the Trading Engine data structure on the other hand can be changed during + the simulation loop, once we discover that all candles from a certain date have benn processed. + Here is the point where we sync one and the other. + */ + let tradingProcessDate = global.REMOVE_TIME(bot.simulationState.tradingEngine.current.episode.processDate.value) + await processSingleFiles() + + if (await processMarketFiles() === false) { + bot.sessionHeartBeat(undefined, undefined, 'Waiting for Data Mining to be run') + callBackFunction(global.DEFAULT_RETRY_RESPONSE) + return + } + /* + This is the Data Structure used at the Simulation with all indicator data. + We start creating it right here. + */ + let chart = {} + /* + Here we check if we need to get Daily Files or not. As an optimization, when + we are running on a Time Frame of 1hs or above, we are not going to load + dependencies on Daily Files. The way we recognize that is by checking if + we alreaady set a value to currentTimeFrame. We are also not going to loop + through days if we are processing market files. + */ + if (currentTimeFrame) { + /* We are processing Market Files */ + /* + With all the indicators data files loaded, we will build the chart object + data structure that will be used in user-defied conditions and formulas. + */ + bot.sessionHeartBeat(undefined, undefined, 'Waking up') + buildCharts(chart) + + if (checkThereAreCandles(chart) === true) { + bot.sessionHeartBeat(undefined, undefined, 'Running') + await generateOutput(chart) + bot.sessionHeartBeat(undefined, undefined, 'Saving') + await writeProcessFiles() + bot.sessionHeartBeat(undefined, undefined, 'Sleeping') + } else { + bot.sessionHeartBeat(undefined, undefined, 'Waiting for Data Mining to be up to date') + callBackFunction(global.DEFAULT_RETRY_RESPONSE) + return + } + + } else { + /* We are processing Daily Files */ + do { + global.EMIT_SESSION_STATUS(bot.SESSION_STATUS, bot.sessionKey) + /* + We update the Trading Process Date with the date calculated at the simulation. + We will use this date to load indicator and output files. After that we will + use it to save Output Files and later the Data Ranges. This is the point where + the date calculated by the Simulation is applied at the Trading Process Level. + */ + tradingProcessDate = global.REMOVE_TIME(bot.simulationState.tradingEngine.current.episode.processDate.value) + + if (checkStopTaskGracefully() === false) { break } + if (checkStopProcessing() === false) { break } + + bot.sessionHeartBeat(undefined, undefined, 'Waking up') + if (await processDailyFiles() === false) { + bot.sessionHeartBeat(undefined, undefined, 'Waiting for Data Mining to be run') + callBackFunction(global.DEFAULT_RETRY_RESPONSE) + return + } + /* + With all the indicators data files loaded, we will build the chart object + data structure that will be used in user-defied conditions and formulas. + */ + buildCharts(chart) + /* + The process of generating the output includes the trading simulation. + */ + if (checkThereAreCandles(chart) === true) { + bot.sessionHeartBeat(undefined, undefined, 'Running') + await generateOutput(chart) + bot.sessionHeartBeat(undefined, undefined, 'Saving') + await writeProcessFiles() + bot.sessionHeartBeat(undefined, undefined, 'Sleeping') + } else { + bot.sessionHeartBeat(undefined, undefined, 'Waiting for Data Mining to be up to date') + callBackFunction(global.DEFAULT_RETRY_RESPONSE) + return + } + /* + If for any reason the session was stopped, we will break this loop and exit the process. + */ + if (bot.STOP_SESSION === true) { break } + /* + When we get to the end of the market, we need to break this process loop in order + to let time pass, new information be collected from the exchange, new data built + into indicators, and eventually a new execution of this process. + */ + if (checkStopHeadOfTheMarket() === false) { break } + + } + while (true) + } + + checkIfSessionMustStop() + /* + Everything worked as expected. We return an OK code and wait for + the Bot Loop to call us again later. + */ + callBackFunction(global.DEFAULT_OK_RESPONSE) + + function checkThereAreCandles(chart) { + let sessionParameters = bot.SESSION.parameters + let propertyName = 'at' + sessionParameters.timeFrame.config.label.replace('-', '') + let candles = chart[propertyName].candles + + if (candles.length === 0) { + return false + } else { + return true + } + } function getContextVariables() { try { let thisReport; - let reportKey; let statusReport; /* We are going to use the start date as beging of market date. */ - contextVariables.dateBeginOfMarket = new Date(bot.SESSION.parameters.timeRange.config.initialDatetime) - + contextVariables.dateBeginOfMarket = global.REMOVE_TIME(bot.SESSION.parameters.timeRange.config.initialDatetime) /* - Here we get the status report from the bot who knows which is the end of the market. + Here we get the status report from the bot who knows which is the end of the market. */ statusReport = statusDependencies.reportsByMainUtility.get("Market Ending Point") if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month. logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Status Report does not exist. Retrying Later. "); callBackFunction(global.DEFAULT_RETRY_RESPONSE); - return; + return } if (statusReport.status === "Status Report is corrupt.") { logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because dependecy Status Report is corrupt. "); callBackFunction(global.DEFAULT_RETRY_RESPONSE); - return; + return } thisReport = statusReport.file; @@ -116,35 +265,24 @@ } logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> customOK = " + customOK.message); callBackFunction(customOK); - return; + return } contextVariables.dateEndOfMarket = new Date(thisReport.lastFile.valueOf()); - /* Validation that the data is not up-to-date. */ - if (bot.tradingProcessDate.valueOf() - global.ONE_DAY_IN_MILISECONDS > contextVariables.dateEndOfMarket.valueOf() && bot.SESSION.type !== "Backtesting Session") { - - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> " + "Head of the market is @ " + contextVariables.dateEndOfMarket); } - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> You need to UPDATE your datasets in order to run a " + bot.SESSION.type) } - - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return; - - } - /* Finally we get our own Status Report. */ statusReport = statusDependencies.reportsByMainUtility.get("Self Reference") if (statusReport === undefined) { // This means the status report does not exist, that could happen for instance at the begining of a month. logger.write(MODULE_NAME, "[WARN] start -> getContextVariables -> Status Report does not exist. Retrying Later. "); callBackFunction(global.DEFAULT_RETRY_RESPONSE); - return; + return } if (statusReport.status === "Status Report is corrupt.") { logger.write(MODULE_NAME, "[ERROR] start -> getContextVariables -> Can not continue because self dependecy Status Report is corrupt. Aborting Process."); callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return; + return } thisReport = statusReport.file; @@ -155,11 +293,10 @@ if (bot.RESUME !== true) { if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining because bot has just started and resume execution was true."); } startFromBegining(); - return; + return } contextVariables.lastFile = new Date(thisReport.lastFile); - processSingleFiles(); - return; + return } else { /* @@ -169,14 +306,13 @@ */ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> Starting from the begining of the market because own status report not found or lastFile was undefined."); } startFromBegining(); - return; + return } function startFromBegining() { contextVariables.lastFile = new Date(contextVariables.dateBeginOfMarket.getUTCFullYear() + "-" + (contextVariables.dateBeginOfMarket.getUTCMonth() + 1) + "-" + contextVariables.dateBeginOfMarket.getUTCDate() + " " + "00:00" + GMT_SECONDS); if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> getContextVariables -> startFromBegining -> contextVariables.lastFile = " + contextVariables.lastFile); } - processSingleFiles(); } } catch (err) { @@ -189,242 +325,135 @@ } } - function processSingleFiles() { - let dependencyIndex = 0; + async function processSingleFiles() { dataFiles = new Map(); - dependencyLoopBody(); - function dependencyLoopBody() { + for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) { let dependency = dataDependenciesModule.nodeArray[dependencyIndex]; let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex]; - getFile(); - function getFile() { - if (datasetModule.node.config.codeName !== "Single-File") { - dependencyControlLoop(); - return - } - - if (dataDependenciesModule.isItADepenency('atAnyTimeFrame', datasetModule.node.parentNode.config.singularVariableName) !== true) { - dependencyControlLoop(); - return - } - - let fileName = "Data.json"; - let filePath = datasetModule.node.parentNode.config.codeName + '/' + datasetModule.node.config.codeName; - datasetModule.getTextFile(filePath, fileName, onFileReceived); - - function onFileReceived(err, text) { - try { - if (LOG_FILE_CONTENT === true) { logger.write(MODULE_NAME, "[INFO] start -> processSingleFiles -> dependencyLoopBody -> getFile -> onFileReceived -> text = " + text); } - - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - if (err.message === 'File does not exist.') { - logger.write(MODULE_NAME, "[ERROR] The file " + filePath + '/' + fileName + ' does not exist and it is required to continue. This process will retry to read it in a while. In the meantime make yourself sure that the process that generates it has ran properly.'); - callBackFunction(global.DEFAULT_RETRY_RESPONSE); - } else { - logger.write(MODULE_NAME, "[ERROR] start -> processSingleFiles -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.message); - callBackFunction(err); - } - return; - } - - let dataFile = JSON.parse(text); - dataFiles.set(dependency.id, dataFile); - - dependencyControlLoop(); - - } - catch (err) { - logger.write(MODULE_NAME, "[ERROR] start -> processSingleFiles -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - } - } + if (datasetModule.node.config.codeName !== "Single-File") { + continue } + + if (dataDependenciesModule.isItADepenency('atAnyTimeFrame', datasetModule.node.parentNode.config.singularVariableName) !== true) { + continue + } + + let fileName = "Data.json"; + let filePath = datasetModule.node.parentNode.config.codeName + '/' + datasetModule.node.config.codeName; + + /* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */ + let response = await asyncGetDatasetFile(datasetModule, filePath, fileName) + + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw (response.err) + } + + let dataFile = JSON.parse(response.text); + dataFiles.set(dependency.id, dataFile); } - function dependencyControlLoop() { - dependencyIndex++ - if (dependencyIndex < dataDependenciesModule.nodeArray.length) { - dependencyLoopBody() - } else { - let mapKey = "Single Files" - multiPeriodDataFiles.set(mapKey, dataFiles) - processMarketFiles() - } - } + let mapKey = "Single Files" + multiPeriodDataFiles.set(mapKey, dataFiles) } - /* - We do market files first since if the simulation is run on daily files, there will be a loop to getch each daus files and we do not need - that loop to reload market files. - */ - function processMarketFiles() { - let n; - timeFramesLoop(); + async function processMarketFiles() { + /* + We do market files first since if the simulation is run on daily files, there will + be a loop to get each of those files and we do not need that loop to reload market files. - function timeFramesLoop() { - /* - We will iterate through all posible timeFrames. - */ - n = 0 // loop Variable representing each possible period as defined at the timeFrames array. - timeFramesLoopBody(); - } - - function timeFramesLoopBody() { + We will iterate through all posible timeFrames. + */ + for (let n = 0; n < global.marketFilesPeriods.length; n++) { const timeFrame = global.marketFilesPeriods[n][0]; const timeFrameLabel = global.marketFilesPeriods[n][1]; - let dependencyIndex = 0; dataFiles = new Map(); if (bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel) { currentTimeFrame = global.marketFilesPeriods[n][0]; currentTimeFrameLabel = global.marketFilesPeriods[n][1]; } - dependencyLoopBody(); - - function dependencyLoopBody() { + for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) { let dependency = dataDependenciesModule.nodeArray[dependencyIndex]; let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex]; - getFile(); - function getFile() { - if (dependency.referenceParent.config.codeName !== "Multi-Period-Market") { - dependencyControlLoop(); - return + if (dependency.referenceParent.config.codeName !== "Multi-Period-Market") { + continue + } + + if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) { + if (!(bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) { + continue } + } - if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) { - if (!(bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) { - dependencyControlLoop(); - return + let fileName = "Data.json"; + let filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel; + + /* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */ + let response = await asyncGetDatasetFile(datasetModule, filePath, fileName) + + if (response.err.message === 'File does not exist.') { + return false + } + + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw (response.err) + } + + let dataFile = JSON.parse(response.text); + let trimmedDataFile = trimDataFile(dataFile, datasetModule.node.parentNode.record) + dataFiles.set(dependency.id, trimmedDataFile); + + function trimDataFile(dataFile, recordDefinition) { + /* + Here we will discard all the records in a file that are outside of the current time range. + We will include the las element previous to the begining of the time range. This is needed + because during the simulation, the current period is not the open one, but the previous to + the open, and if we do not include the previous to the initial datetime there will be no + current objects at the begining of the simulation for many time frames. + */ + let beginIndex + let endIndex + let result = [] + for (let i = 0; i < recordDefinition.properties.length; i++) { + let property = recordDefinition.properties[i] + if (property.config.codeName === 'begin') { + beginIndex = i + } + if (property.config.codeName === 'end') { + endIndex = i } } - - let fileName = "Data.json"; - let filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel; - datasetModule.getTextFile(filePath, fileName, onFileReceived); - - function onFileReceived(err, text) { - try { - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - if (err.message === 'File does not exist.') { - logger.write(MODULE_NAME, "[ERROR] The file " + filePath + '/' + fileName + ' does not exist and it is required to continue. This process will retry to read it in a while. In the meantime make yourself sure that the process that generates it has ran properly.'); - callBackFunction(global.DEFAULT_RETRY_RESPONSE); - } else { - logger.write(MODULE_NAME, "[ERROR] start -> processMarketFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.message); - callBackFunction(err); - } - return; - } - - let dataFile = JSON.parse(text); - let trimmedDataFile = trimDataFile(dataFile, datasetModule.node.parentNode.record) - dataFiles.set(dependency.id, trimmedDataFile); - - dependencyControlLoop(); - } - catch (err) { - logger.write(MODULE_NAME, "[ERROR] start -> processMarketFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - } + for (let i = 0; i < dataFile.length; i++) { + let dataRecord = dataFile[i] + let begin = dataRecord[beginIndex] + let end = dataRecord[endIndex] + if (end + timeFrame < bot.SESSION.parameters.timeRange.config.initialDatetime - 1) { continue } // /1 because we need the previous closed element + if (begin > bot.SESSION.parameters.timeRange.config.finalDatetime) { continue } + result.push(dataRecord) } + return result } } - function dependencyControlLoop() { - dependencyIndex++; - if (dependencyIndex < dataDependenciesModule.nodeArray.length) { - dependencyLoopBody(); - } else { - let mapKey = global.marketFilesPeriods[n][1]; - multiPeriodDataFiles.set(mapKey, dataFiles) - timeFramesControlLoop(); - } - } - } - - function timeFramesControlLoop() { - n++; - if (n < global.marketFilesPeriods.length) { - timeFramesLoopBody(); - } else { - processDailyFiles(); - } + let mapKey = global.marketFilesPeriods[n][1]; + multiPeriodDataFiles.set(mapKey, dataFiles) } + return true } - function processDailyFiles() { - let n = 0; - if (currentTimeFrame) { - buildCharts() - return - } - bot.tradingProcessDate = new Date(contextVariables.lastFile.valueOf() - global.ONE_DAY_IN_MILISECONDS); // Go back one day to start well when we advance time at the begining of the loop. - advanceTime(); + async function processDailyFiles() { + /* Telling the world we are alive and doing well and which date we are processing right now. */ + let processingDateString = tradingProcessDate.getUTCFullYear() + '-' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(tradingProcessDate.getUTCDate(), 2); + bot.processHeartBeat(processingDateString, undefined, "Running...") - function advanceTime() { - bot.tradingProcessDate = new Date(bot.tradingProcessDate.valueOf() + global.ONE_DAY_IN_MILISECONDS); - previousDay = new Date(bot.tradingProcessDate.valueOf() - global.ONE_DAY_IN_MILISECONDS); - - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> bot.tradingProcessDate = " + bot.tradingProcessDate); } - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> previousDay = " + previousDay); } - - /* Validation that we are not going past the user defined end date. */ - if (bot.tradingProcessDate.valueOf() >= bot.SESSION.parameters.timeRange.config.finalDatetime) { - - const logText = "User Defined End Datetime reached @ " + previousDay.getUTCFullYear() + "/" + (previousDay.getUTCMonth() + 1) + "/" + previousDay.getUTCDate() + "."; - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> " + logText); } - - bot.SESSION.stop(logText) - callBackFunction(global.DEFAULT_OK_RESPONSE); - return; - } - - /* Validation that we are not going past the head of the market. */ - if (bot.tradingProcessDate.valueOf() > contextVariables.dateEndOfMarket.valueOf() + global.ONE_DAY_IN_MILISECONDS - 1) { - - const logText = "Head of the market found @ " + previousDay.getUTCFullYear() + "/" + (previousDay.getUTCMonth() + 1) + "/" + previousDay.getUTCDate() + "."; - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> advanceTime -> " + logText); } - - callBackFunction(global.DEFAULT_OK_RESPONSE); - return; - } - - checkStopTaskGracefully(); - } - - function checkStopTaskGracefully() { - /* Validation that we dont need to stop. */ - if (global.STOP_TASK_GRACEFULLY === true) { - callBackFunction(global.DEFAULT_OK_RESPONSE); - return; - } - checkStopProcessing(); - } - - function checkStopProcessing() { - /* Validation that we dont need to stop. */ - if (bot.STOP_SESSION === true) { - callBackFunction(global.DEFAULT_OK_RESPONSE); - return; - } - timeFramesLoop(); - } - - function timeFramesLoop() { - /* Telling the world we are alive and doing well */ - let processingDate = bot.tradingProcessDate.getUTCFullYear() + '-' + utilities.pad(bot.tradingProcessDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(bot.tradingProcessDate.getUTCDate(), 2); - bot.processHeartBeat(processingDate) - /* - We will iterate through all posible timeFrames. - */ - n = 0 // loop Variable representing each possible period as defined at the timeFrames array. - timeFramesLoopBody(); - } - - function timeFramesLoopBody() { + /* + We will iterate through all posible timeFrames. + */ + for (let n = 0; n < global.dailyFilePeriods.length; n++) { const timeFrame = global.dailyFilePeriods[n][0]; const timeFrameLabel = global.dailyFilePeriods[n][1]; @@ -435,8 +464,7 @@ if (period === timeFrameLabel) { validPeriod = true } } if (validPeriod === false) { - timeFramesControlLoop(); - return; + continue } } @@ -445,108 +473,99 @@ currentTimeFrameLabel = global.dailyFilePeriods[n][1]; } - let dependencyIndex = 0; dataFiles = new Map(); - dependencyLoopBody(); - function dependencyLoopBody() { + /* + We will iterate through all dependencies, in order to load the + files that later will end up at the chart data structure. + */ + for (let dependencyIndex = 0; dependencyIndex < dataDependenciesModule.nodeArray.length; dependencyIndex++) { let dependency = dataDependenciesModule.nodeArray[dependencyIndex]; let datasetModule = dataDependenciesModule.dataSetsModulesArray[dependencyIndex]; - let file - getFile() - function getFile() { - if (dependency.referenceParent.config.codeName !== "Multi-Period-Daily") { - dependencyControlLoop(); - return - } + if (dependency.referenceParent.config.codeName !== "Multi-Period-Daily") { + continue + } - if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) { - if (!(bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) { - dependencyControlLoop(); - return - } - } - - let dateForPath = bot.tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(bot.tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(bot.tradingProcessDate.getUTCDate(), 2); - let filePath - if (dependency.referenceParent.config.codeName === "Multi-Period-Daily") { - filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel + "/" + dateForPath; - } else { - filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + dateForPath; - } - let fileName = "Data.json"; - - datasetModule.getTextFile(filePath, fileName, onFileReceived); - - function onFileReceived(err, text) { - try { - if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') { - logger.write(MODULE_NAME, "[ERROR] The file " + filePath + '/' + fileName + ' does not exist and it is required to continue. This process will retry to read it in a while. In the meantime make yourself sure that the process that generates it has ran properly.'); - callBackFunction(global.DEFAULT_RETRY_RESPONSE); - return; - } - - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> Not Ok -> err = " + err.code); - callBackFunction(err); - return; - } - - file = JSON.parse(text); - dataFiles.set(dependency.id, file); - dependencyControlLoop(); - - } - catch (err) { - logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> timeFramesLoopBody -> dependencyLoopBody -> getFile -> onFileReceived -> err = " + err.stack); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - } + if (dataDependenciesModule.isItADepenency(timeFrameLabel, datasetModule.node.parentNode.config.singularVariableName) !== true) { + if (!(bot.SESSION.parameters.timeFrame.config.label === timeFrameLabel && datasetModule.node.parentNode.config.pluralVariableName === 'candles')) { + continue } } - } - function dependencyControlLoop() { - dependencyIndex++; - - if (dependencyIndex < dataDependenciesModule.nodeArray.length) { - dependencyLoopBody(); + let dateForPath = tradingProcessDate.getUTCFullYear() + '/' + utilities.pad(tradingProcessDate.getUTCMonth() + 1, 2) + '/' + utilities.pad(tradingProcessDate.getUTCDate(), 2); + let filePath + if (dependency.referenceParent.config.codeName === "Multi-Period-Daily") { + filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + timeFrameLabel + "/" + dateForPath; } else { - let mapKey = global.dailyFilePeriods[n][1]; - multiPeriodDataFiles.set(mapKey, dataFiles) - timeFramesControlLoop(); + filePath = dependency.referenceParent.parentNode.config.codeName + '/' + dependency.referenceParent.config.codeName + "/" + dateForPath; } + let fileName = "Data.json"; + + /* We cut the async calls via callBacks at this point, so as to have a clearer code upstream */ + let response = await asyncGetDatasetFile(datasetModule, filePath, fileName) + + if (response.err.message === 'File does not exist.') { + return false + } + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw (response.err) + } + + let dataFile = JSON.parse(response.text); + dataFiles.set(dependency.id, dataFile); } + + let mapKey = global.dailyFilePeriods[n][1]; + multiPeriodDataFiles.set(mapKey, dataFiles) } + return true + } - function timeFramesControlLoop() { - n++; - if (n < global.dailyFilePeriods.length) { - timeFramesLoopBody(); - } else { - n = 0; - if (currentTimeFrame !== undefined) { - buildCharts(advanceTime); - } else { - logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> timeFramesControlLoop -> Time Frame not Recognized. Can not continue."); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - } - } + function checkStopHeadOfTheMarket() { + /* + We need to check if we have reached the head of the market in order to know + when to break the Daily Files Process loop and give time for a new candles / + indicators to be built and continue the processing once this process is called + again. + */ + if (bot.simulationState.tradingEngine.current.episode.headOfTheMarket.value === true) { + return false } } - function buildCharts(advanceTime) { + function checkStopTaskGracefully() { + /* Validation that we dont need to stop. */ + if (global.STOP_TASK_GRACEFULLY === true) { + return false + } + } + + function checkStopProcessing() { + /* Validation that we dont need to stop. */ + if (bot.STOP_SESSION === true) { + return false + } + } + + function buildCharts(chart) { const COMMONS = require('../Commons.js') let commons = COMMONS.newCommons(bot, logger, UTILITIES, FILE_STORAGE) - let chart = {} let mainDependency = {} /* The first phase here is about checking that we have everything we need at the definition level. */ - let dataDependencies = bot.processNode.referenceParent.processDependencies.dataDependencies + let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency') + /* + We will filter ourt declared dependencies that are not present in the workspace. + This will allow the user to have less Data Mines loaded at the workspace + that the ones that a Trading Mine depends on. + */ + dataDependencies = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(dataDependencies) + if (commons.validateDataDependencies(dataDependencies, callBackFunction) !== true) { return } - let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets + let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset') if (commons.validateOutputDatasets(outputDatasets, callBackFunction) !== true) { return } /* The second phase is about transforming the inputs into a format that can be used to apply the user defined code. */ @@ -586,177 +605,159 @@ let propertyName = 'atAnyTimeFrame' chart[propertyName] = products } + } - tradingOutputModule.start( + async function generateOutput(chart) { + await tradingOutputModule.start( chart, currentTimeFrame, currentTimeFrameLabel, - bot.tradingProcessDate, - onOutputGenerated); + tradingProcessDate + ) - function onOutputGenerated(err) { - try { - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - callBackFunction(err); - return; - } - - bot.FIRST_EXECUTION = false // From here on, all other loops executions wont be the first execution. - - if (currentTimeFrame > global.dailyFilePeriods[0][0]) { - writeMarketStatusReport(onMarketStatusReport) - } else { - writeDataRanges(currentTimeFrameLabel, onWritten); - } - - function onWritten(err) { - try { - - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> onWritten -> err = " + err.stack); - callBackFunction(err); - return; - } - - writeDailyStatusReport(bot.tradingProcessDate, onDailyStatusReport); - } catch (err) { - logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> onWritten -> err = " + err.stack); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - } - } - - function onDailyStatusReport() { - advanceTime(); - } - - function onMarketStatusReport() { - /* This is where we check if we need reached the user defined end datetime. */ - const now = new Date() - if (now.valueOf() >= bot.SESSION.parameters.timeRange.config.finalDatetime) { - const logText = "User Defined End Datetime reached @ " + now.getUTCFullYear() + "/" + (now.getUTCMonth() + 1) + "/" + now.getUTCDate() + "."; - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> onMarketStatusReport -> " + logText); } - - bot.SESSION.stop('because it just finished.') - callBackFunction(global.DEFAULT_OK_RESPONSE); - return; - } - if (bot.SESSION.type === 'Backtesting Session') { - bot.SESSION.stop('Backtesting Session over Market Files Finished.') - } - callBackFunction(global.DEFAULT_OK_RESPONSE); - } - } - catch (err) { - logger.write(MODULE_NAME, "[ERROR] start -> processDailyFiles -> buildCharts -> onOutputGenerated -> err = " + err.stack); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - } - } - } - - function writeDataRanges(currentTimeFrameLabel, callBack) { - let outputDatasetIndex = -1; - controlLoop(); - - function productLoopBody() { - let productCodeName = bot.processNode.referenceParent.processOutput.outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName; - writeDataRange(contextVariables.dateBeginOfMarket, bot.tradingProcessDate, productCodeName, currentTimeFrameLabel, controlLoop); - } - - function controlLoop() { - outputDatasetIndex++; - if (outputDatasetIndex < bot.processNode.referenceParent.processOutput.outputDatasets.length) { - productLoopBody(); - } else { - callBack(global.DEFAULT_OK_RESPONSE); - } - } - } - - function writeDataRange(pBegin, pEnd, productCodeName, currentTimeFrameLabel, callBack) { - let dataRange = { - begin: pBegin.valueOf(), - end: pEnd.valueOf() + global.ONE_DAY_IN_MILISECONDS - }; - - let fileContent = JSON.stringify(dataRange); - let fileName = '/Data.Range.json'; - let filePath = bot.filePathRoot + "/Output/" + bot.SESSION.folderName + "/" + productCodeName + "/" + 'Multi-Period-Daily' + fileName; - - fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated); - - function onFileCreated(err) { - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - logger.write(MODULE_NAME, "[ERROR] start -> writeDataRange -> onFileCreated -> err = " + err.stack); - callBack(err); - return; - } - - if (LOG_FILE_CONTENT === true) { - logger.write(MODULE_NAME, "[INFO] start -> writeDataRange -> onFileCreated -> Content written = " + fileContent); - } - - let key = bot.dataMine + "-" + bot.codeName + "-" + productCodeName + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset - let event = { - dateRange: dataRange - } - - global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Data Range Updated', event) - - callBack(global.DEFAULT_OK_RESPONSE); - } - } - - function writeDailyStatusReport(lastFileDate, callBack) { - let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName - let thisReport = statusDependencies.statusReports.get(reportKey); - - thisReport.file.lastExecution = bot.currentDaytime; - thisReport.file.lastFile = lastFileDate; - thisReport.file.simulationState = bot.simulationState; - thisReport.save(callBack); - } - - function writeMarketStatusReport(callBack) { - let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName - let thisReport = statusDependencies.statusReports.get(reportKey); - - thisReport.file.lastExecution = bot.processDatetime; - thisReport.file.simulationState = bot.simulationState; - - logger.newInternalLoop(bot.codeName, bot.process, bot.tradingProcessDate); - thisReport.save(callBack); - } - - function trimDataFile(dataFile, recordDefinition) { - /* - Here we will discard all the records in a file that are outside of the current time range. + /* + From here on, all other loops executions wont be the first execution and also + we will consider that it is not resuming a previous execution as well. */ - let beginIndex - let endIndex - let result = [] - for (let i = 0; i < recordDefinition.properties.length; i++) { - let property = recordDefinition.properties[i] - if (property.config.codeName === 'begin') { - beginIndex = i + bot.FIRST_EXECUTION = false + bot.RESUME = false + } + + async function writeProcessFiles() { + let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset') + + await writeTimeFramesFiles(currentTimeFrame, currentTimeFrameLabel) + await writeDataRanges() + + if (currentTimeFrame > global.dailyFilePeriods[0][0]) { + await writeMarketStatusReport(currentTimeFrameLabel) + } else { + await writeDailyStatusReport(currentTimeFrameLabel) + } + + async function writeDataRanges() { + for ( + let outputDatasetIndex = 0; + outputDatasetIndex < outputDatasets.length; + outputDatasetIndex++ + ) { + let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName; + await writeDataRange(productCodeName); } - if (property.config.codeName === 'end') { - endIndex = i + + async function writeDataRange(productCodeName) { + let dataRange = { + begin: contextVariables.dateBeginOfMarket.valueOf(), + end: tradingProcessDate.valueOf() + global.ONE_DAY_IN_MILISECONDS + } + + let fileContent = JSON.stringify(dataRange); + let fileName = '/Data.Range.json'; + let filePath = bot.filePathRoot + "/Output/" + bot.SESSION.folderName + "/" + productCodeName + "/" + 'Multi-Period-Daily' + fileName; + + let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n') + + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw (response.err) + } + + /* + Raise the event that the Data Range was Updated. + */ + let key = bot.dataMine + "-" + bot.codeName + "-" + productCodeName + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset + let event = { + dateRange: dataRange + } + + global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Data Range Updated', event) } } - for (let i = 0; i < dataFile.length; i++) { - let dataRecord = dataFile[i] - let begin = dataRecord[beginIndex] - let end = dataRecord[endIndex] - if (end < bot.SESSION.parameters.timeRange.config.initialDatetime) { continue } - if (begin > bot.SESSION.parameters.timeRange.config.finalDatetime) { continue } - result.push(dataRecord) + + async function writeTimeFramesFiles(currentTimeFrame, currentTimeFrameLabel) { + + for ( + let outputDatasetIndex = 0; + outputDatasetIndex < outputDatasets.length; + outputDatasetIndex++ + ) { + let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName; + await writeTimeFramesFile(currentTimeFrameLabel, productCodeName, 'Multi-Period-Daily') + await writeTimeFramesFile(currentTimeFrameLabel, productCodeName, 'Multi-Period-Market') + + async function writeTimeFramesFile(currentTimeFrameLabel, productCodeName, processType) { + + let timeFramesArray = [] + timeFramesArray.push(currentTimeFrameLabel) + + let fileContent = JSON.stringify(timeFramesArray) + let fileName = '/Time.Frames.json'; + + let filePath = bot.filePathRoot + "/Output/" + bot.SESSION.folderName + "/" + productCodeName + "/" + processType + fileName; + + let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n') + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw (response.err) + } + } + } + } + + async function writeDailyStatusReport(currentTimeFrameLabel) { + let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName + let thisReport = statusDependencies.statusReports.get(reportKey) + + thisReport.file.lastExecution = bot.currentDaytime + thisReport.file.lastFile = tradingProcessDate + thisReport.file.simulationState = bot.simulationState + thisReport.file.timeFrames = currentTimeFrameLabel + await thisReport.asyncSave() + } + + async function writeMarketStatusReport(currentTimeFrameLabel) { + let reportKey = bot.dataMine + "-" + bot.codeName + "-" + bot.processNode.referenceParent.config.codeName + let thisReport = statusDependencies.statusReports.get(reportKey); + + thisReport.file.lastExecution = bot.processDatetime + thisReport.file.simulationState = bot.simulationState + thisReport.file.timeFrames = currentTimeFrameLabel + + logger.newInternalLoop(bot.codeName, bot.process, tradingProcessDate) + await thisReport.asyncSave() + } + } + + function checkIfSessionMustStop() { + + if (bot.SESSION.type === 'Backtesting Session') { + /* + Backtests needs only one execution of this process to complete. + */ + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] checkIfSessionMustStop -> Backtesting Session Finished. Stopping the Session now. ') } + bot.SESSION.stop('Backtesting Session Finished.') } - return result } } catch (err) { - logger.write(MODULE_NAME, "[ERROR] start -> err = " + err.stack); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); + /* An unhandled exception occured. in this case we return Fail and log the stack. */ + if (err.stack) { + logger.write(MODULE_NAME, "[ERROR] start -> Unhandled Exception. Will Abort this process. err = " + err.stack); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } + + /* Some expected file was not found. We will return a RETRY code and move on. */ + if ((err.result === "Fail Because" && err.message === "File does not exist.") || err.code === 'The specified key does not exist.') { + logger.write(MODULE_NAME, "[ERROR] File not Found. Will Retry the Process Loop.") + callBackFunction(global.DEFAULT_RETRY_RESPONSE); + return + } + + /* Some other handled exception occured. We return Fail and move on. */ + if (err.result !== global.DEFAULT_OK_RESPONSE.result) { + logger.write(MODULE_NAME, "[ERROR] start -> Handled Exception. Will Abort this process. err = " + err.message); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } } } @@ -764,4 +765,22 @@ str = str.toString(); return str.length < max ? pad(" " + str, max) : str; } -}; + + async function asyncGetDatasetFile(datasetModule, filePath, fileName) { + + let promise = new Promise((resolve, reject) => { + + datasetModule.getTextFile(filePath, fileName, onFileReceived); + function onFileReceived(err, text) { + + let response = { + err: err, + text: text + } + resolve(response) + } + }); + + return promise + } +} diff --git a/LowFrequencyTrading/TradingRecords.js b/LowFrequencyTrading/TradingRecords.js index 789d67feb..f3de8553c 100644 --- a/LowFrequencyTrading/TradingRecords.js +++ b/LowFrequencyTrading/TradingRecords.js @@ -37,7 +37,8 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { we have a pointer to the node that have the information we need to extract. Later, based on the product record definition we will extract each individual value. */ - let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets + let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset') + for (let i = 0; i < outputDatasets.length; i++) { let outputDatasetNode = outputDatasets[i] let dataset = outputDatasetNode.referenceParent @@ -53,9 +54,17 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { } function persistRecords() { + /* + There are a few products configured to be saved only at an specific cycle. + */ + if (product.config.saveAtCycle !== undefined) { + if (tradingEngine.current.episode.cycle.value !== product.config.saveAtCycle) { + return + } + } /* Clean the file from information of previous executions */ - pruneOutputFile(product, outputDatasetArray) + //pruneOutputFile(product, outputDatasetArray, tradingEngine.current.episode.candle.end.value) /* Clean Open Records */ if (product.config.saveAsObjects === true) { @@ -64,6 +73,7 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { if (recordProperty.config.codeName === product.config.propertyNameThatDefinesObject) { /* Remove Open Records */ spliceOpenRecords(j, product, outputDatasetArray) + break } } } @@ -71,7 +81,12 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { /* The product root can be a node or a node property of type array. */ - let productRoot = eval(product.config.nodePath) + let productRoot + try { + productRoot = eval(product.config.nodePath) + } catch (err) { + badDefinitionUnhandledException(err, 'Cannot get productRoot -> nodePath = ' + product.config.nodePath, product, undefined) + } if (product.config.nodePathType === 'array') { /* @@ -84,7 +99,14 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { */ let productRootNode = productRoot[index] let record = scanRecordDefinition(product, productRootNode, index) - persistIndividualRecord(record, product, outputDatasetArray) + if (record !== undefined) { + /* + We will add the index value to the record itself, so that the plotter can know to which + brach of the trading engine data structure it belongs. + */ + record.push(index) + persistIndividualRecord(record, product, outputDatasetArray) + } } } else { /* @@ -96,7 +118,9 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { */ let productRootNode = productRoot let record = scanRecordDefinition(product, productRootNode) - persistIndividualRecord(record, product, outputDatasetArray) + if (record !== undefined) { + persistIndividualRecord(record, product, outputDatasetArray) + } } } } @@ -122,10 +146,7 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { try { propertyRootNode = eval(recordProperty.config.nodePath) } catch (err) { - logger.write(MODULE_NAME, "[ERROR] appendRecords -> scanRecordDefinition -> Error at Record Property Definition -> recordProperty.name = " + recordProperty.name); - logger.write(MODULE_NAME, "[ERROR] appendRecords -> scanRecordDefinition -> Error at Record Property Definition -> recordProperty.config.nodePath = " + recordProperty.config.nodePath); - logger.write(MODULE_NAME, "[ERROR] appendRecords -> scanRecordDefinition -> Error at Record Property Definition -> err.stack = " + err.stack); - throw 'Can not continue with a Definition Error like this.' + badDefinitionUnhandledException(err, 'Error Evaluation Record Property nodePath -> nodePath = ' + recordProperty.config.nodePath, product, recordProperty) } } /* @@ -133,7 +154,12 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { We will use the codeName of the Record Property to match it with the any of the properties of the Root Node to get the Target Node. */ - let targetNode = propertyRootNode[recordProperty.config.codeName] + let targetNode + try { + targetNode = propertyRootNode[recordProperty.config.codeName] + } catch (err) { + badDefinitionUnhandledException(err, 'Error setting Target Node.', product, recordProperty) + } /* If the codeName of the Record Property can not match the name of the property at the target node, the user can explicitly specify the property name at the configuration, @@ -149,7 +175,11 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { we use the Index value we find at the configuration of the Record Property. */ if (recordProperty.config.index !== undefined) { - targetNode = targetNode[recordProperty.config.index] + try { + targetNode = targetNode[recordProperty.config.index] + } catch (err) { + badDefinitionUnhandledException(err, 'Error setting Target Node.', product, recordProperty) + } } /* By Default the value is extracted from the value property of the Target Node. @@ -167,7 +197,11 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { value = targetNode.value if (recordProperty.config.decimals !== undefined) { - value = Number(value.toFixed(recordProperty.config.decimals)) + try { + value = Number(value.toFixed(recordProperty.config.decimals)) + } catch (err) { + badDefinitionUnhandledException(err, 'Error applying configured decimals.', product, recordProperty) + } } } else { /* @@ -177,6 +211,13 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { value = targetNode } } + + /* We are not going to add records where the begin or end are missing */ + if ( + recordProperty.config.codeName === 'begin' && value === 0 || + recordProperty.config.codeName === 'end' && value === 0 + ) { return } + if (recordProperty.config.isString !== true && Array.isArray(value) !== true) { value = safeNumericValue(value) } @@ -198,27 +239,36 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { let propertyValue = record[j] if (bot.processingDailyFiles) { - /* - When dealing with Daily Files, we need to avoid to write an open object at the last 'candle' of the day, - since the object will be duplicated on the next day. How do we know we are positioned at the last candle - of the day? Easy: the end of the candle must be 1 millisecod before the next day. That happens at any - time frame. - */ - let currentDay = new Date(tradingEngine.current.candle.end.value) - let nextDay = new Date(tradingEngine.current.candle.end.value + 1) - if (currentDay.getUTCDate() !== nextDay.getUTCDate()) { - /* - We will save the object only if it is closed, becasuse we are at the last candle of the day. + if (product.config.doNotCutObjectInDays !== true) { + /* + By default we will cut objects in days. */ - if (propertyValue === product.config.propertyValueThatClosesObject) { + if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) { outputDatasetArray.push(record) } } else { /* - When we are not at the end of the day, we will save the object normally, like in market files. + When dealing with Daily Files, we need to avoid to write an open object at the last 'candle' of the day, + since the object will be duplicated on the next day. How do we know we are positioned at the last candle + of the day? Easy: the end of the candle must be 1 millisecod before the next day. That happens at any + time frame. */ - if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) { - outputDatasetArray.push(record) + let currentDay = new Date(tradingEngine.current.episode.candle.end.value) + let nextDay = new Date(tradingEngine.current.episode.candle.end.value + 1) + if (currentDay.getUTCDate() !== nextDay.getUTCDate()) { + /* + We will save the object only if it is closed, becasuse we are at the last candle of the day. + */ + if (propertyValue === product.config.propertyValueThatClosesObject) { + outputDatasetArray.push(record) + } + } else { + /* + When we are not at the end of the day, we will save the object normally, like in market files. + */ + if (propertyValue !== product.config.propertyValueThatPreventsSavingObject) { + outputDatasetArray.push(record) + } } } } @@ -273,7 +323,7 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { return value } - function pruneOutputFile(product, outputFile) { + function pruneOutputFile(product, outputFile, currentEnd) { if (outputFile.isPrunned === true) { return } /* When a session is resumed, we will be potentially reading output files belonging to a previous session execution. @@ -287,9 +337,14 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { let recordProperty = product.record.properties[j] if (recordProperty.config.codeName === 'end') { let end = record[j] - if (end >= tradingEngine.current.candle.end.value) { + if (end >= currentEnd) { outputFile.splice(i, 1) - pruneOutputFile(product, outputFile) + /* + This will execute the next prune call in the next iteration of the NodeJs event loop + allowing for other callbacks to be executed. It also prevents the error + 'Maximum call stack size exceeded', since the call is not placed at the call stack. + */ + setImmediate(pruneOutputFile, product, outputFile, currentEnd) return } } @@ -318,4 +373,21 @@ exports.newTradingRecords = function newTradingRecords(bot, logger) { } } } + + function badDefinitionUnhandledException(err, message, product, recordProperty) { + logger.write(MODULE_NAME, "[ERROR] appendRecords -> " + message); + logger.write(MODULE_NAME, "[ERROR] appendRecords -> product.name = " + product.name); + logger.write(MODULE_NAME, "[ERROR] appendRecords -> product.config = " + JSON.stringify(product.config)); + + if (recordProperty) { + logger.write(MODULE_NAME, "[ERROR] appendRecords -> recordProperty.name = " + recordProperty.name); + logger.write(MODULE_NAME, "[ERROR] appendRecords -> recordProperty.config.codeName = " + recordProperty.config.codeName); + logger.write(MODULE_NAME, "[ERROR] appendRecords -> recordProperty.config = " + JSON.stringify(recordProperty.config)); + } + + if (err) { + logger.write(MODULE_NAME, "[ERROR] appendRecords -> err.stack = " + err.stack); + } + throw 'Can not continue with a Definition Error like this.' + } } \ No newline at end of file diff --git a/LowFrequencyTrading/TradingSimulation.js b/LowFrequencyTrading/TradingSimulation.js index 58470f756..186f2872e 100644 --- a/LowFrequencyTrading/TradingSimulation.js +++ b/LowFrequencyTrading/TradingSimulation.js @@ -1,4 +1,4 @@ -exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILITIES) { +exports.newTradingSimulation = function newTradingSimulation(bot, logger, tradingEngineModule, UTILITIES) { /* This Module represents the trading simulacion. Escentially a loop through a set of candles and the execution at each loop cycle of the Trading System Protocol. @@ -19,11 +19,11 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT thisObject = undefined } - function runSimulation( + async function runSimulation( chart, outputDatasetsMap, - callback, - callBackFunction) { + writeFiles, + ) { try { let tradingSystem = bot.simulationState.tradingSystem @@ -33,19 +33,7 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> initialDatetime = ' + sessionParameters.timeRange.config.initialDatetime) } if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> finalDatetime = ' + sessionParameters.timeRange.config.finalDatetime) } - /* Stop Loss Management TODO */ - 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 TODO */ - 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 - /* 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() - const TRADING_RECORDS_MODULE = require('./TradingRecords.js') let tradingRecordsModule = TRADING_RECORDS_MODULE.newTradingRecords(bot, logger) tradingRecordsModule.initialize(outputDatasetsMap) @@ -54,6 +42,10 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT let tradingSystemModule = TRADING_SYSTEM_MODULE.newTradingSystem(bot, logger, tradingEngineModule) tradingSystemModule.initialize() + const TRADING_EPISODE_MODULE = require('./TradingEpisode.js') + let tradingEpisodeModule = TRADING_EPISODE_MODULE.newTradingEpisode(bot, logger, tradingEngineModule) + tradingEpisodeModule.initialize() + /* 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 @@ -61,163 +53,261 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT /* Variables needed for heartbeat functionality */ let heartBeatDate let previousHeartBeatDate + let firstLoopExecution = true + /* + Estimation of the Initial Candle to Process in this Run. + */ + let initialCandle + if (bot.FIRST_EXECUTION === true && bot.RESUME === false) { + /* Estimate Initial Candle based on the timeRage configured for the session. */ + let firstEnd = candles[0].end + let targetEnd = sessionParameters.timeRange.config.initialDatetime + let diff = targetEnd - firstEnd + let amount = diff / sessionParameters.timeFrame.config.value - /* This determines where we are standing */ - bot.sessionAndMarketBaseAssetsAreEqual = (sessionParameters.sessionBaseAsset.referenceParent.referenceParent.config.codeName === bot.market.baseAsset) - - initializeLoop() - - function initializeLoop() { - if (bot.FIRST_EXECUTION === false && tradingEngine.last.candle !== undefined) { - /* Estimate the Initial Candle based on the last candle saved at tradingEngine */ - let firstBegin = candles[0].begin - let lastBegin = tradingEngine.last.candle.begin - let diff = lastBegin - firstBegin - let amount = diff / sessionParameters.timeFrame.config.value - - tradingEngine.current.candle.index.value = Math.trunc(amount) + 1 // Because we need to start from the next candle - if (tradingEngine.current.candle.index.value < 0 || tradingEngine.current.candle.index.value > candles.length - 1) { - logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> Cannot resume simulation.') - logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> tradingEngine.current.candle.index.value = ' + tradingEngine.current.candle.index.value) - logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> firstBegin = ' + firstBegin) - logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> lastBegin = ' + lastBegin) - logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> diff = ' + diff) - logger.write(MODULE_NAME, '[ERROR] runSimulation -> initializeLoop -> amount = ' + amount) - - callBackFunction(global.DEFAULT_FAIL_RESPONSE) - return - } - } else { - /* Estimate Initial Candle based on the timeRage configured for the session. */ - let firstEnd = candles[0].end - let targetEnd = sessionParameters.timeRange.config.initialDatetime - let diff = targetEnd - firstEnd - let amount = diff / sessionParameters.timeFrame.config.value - - tradingEngine.current.candle.index.value = Math.trunc(amount) - if (tradingEngine.current.candle.index.value < 0) { tradingEngine.current.candle.index.value = 0 } - if (tradingEngine.current.candle.index.value > candles.length - 1) { - /* - This will happen when the sessionParameters.timeRange.config.initialDatetime is beyond the last candle available, - meaning that the dataSet needs to be updated with more up-to-date data. - */ - tradingEngine.current.candle.index.value = candles.length - 1 - } + initialCandle = Math.trunc(amount) + if (initialCandle < 0) { initialCandle = 0 } + if (initialCandle > candles.length - 1) { + /* + This will happen when the sessionParameters.timeRange.config.initialDatetime is beyond the last candle available, + meaning that the dataSet needs to be updated with more up-to-date data. + */ + bot.SESSION.stop('Data is not up-to-date enough. Please start the Masters Data Mining Operation.') + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] runSimulation -> Data is not up-to-date enough. Stopping the Session now. ' ) } + return } + } else { - loop() + /* + In this case we already have at the last candle index the next candle to be + processed. We will just continue with this candle. + */ + initialCandle = tradingEngine.current.episode.candle.index.value } + /* + Main Simulation Loop + */ + /* We are going to use this to exit the loop if needed. */ + let breakLoop = false - async function loop() { - 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] // This is the current candle the Simulation is working at. + /* + We will assume that we are at the head of the market here. We do this + because the loop could be empty and no validation is going to run. If the + loop is not empty, then the lascCandle() check will override this value + depending on if we really are at the head of the market or not. + */ + tradingEngine.current.episode.headOfTheMarket.value = true + /* + This is the main simulation loop. It will go through the initial candle + until one less than the last candle available. We will never process the last + candle available since it is not considered a closed candle, but a candle + that still can change. So effectively will be processing all closed candles. + */ + for (let i = initialCandle; i < candles.length - 1; i++) { + tradingEngine.current.episode.candle.index.value = i + + /* This is the current candle the Simulation is working at. */ + let candle = candles[tradingEngine.current.episode.candle.index.value] 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()) } 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. - if (checkIfWeAreAtTheHeadOfTheMarket() === false) { - afterLoop() - return - } - - if (checkMinimunAndMaximunBalance() === false) { - afterLoop() - return - } - - if (checkInitialAndFinalDatetime() === false) { - controlLoop() - return - } - + /* We emit a heart beat so that the UI can now where we are at the overal process.*/ heartBeat() + + if (firstLoopExecution === true) { + tradingEpisodeModule.openEpisode() + firstLoopExecution = false + } + + if (checkInitialDatetime() === false) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Candle Before the Initia Date Time @ ' + (new Date(candle.begin)).toLocaleString()) } + continue + } + positionChartAtCurrentCandle() - tradingSystemModule.reset(chart) - tradingSystemModule.mantainPositions() - tradingSystemModule.mantainStrategies() - tradingSystemModule.mantainOrders() - tradingEngineModule.updateEpisodeCountersAndStatistics() - tradingEngineModule.updateDistanceToEventsCounters() + /* The chart was recalculated based on the current candle. */ + tradingSystemModule.updateChart(chart) - /* Run the Trigger Stage */ - tradingSystemModule.triggerStage() + /* + Do the stuff needed previous to the run like + Episode Counters and Statistics update. Mantaince is done + once per simulation candle. + */ + tradingSystemModule.mantain() + tradingEpisodeModule.mantain() + tradingEngineModule.mantain() - /* Run the Open Stage */ - await tradingSystemModule.openStage() + /* + Run the first cycle of the Trading System. In this first cycle we + give some room so that orders can be canceled or filled and we can + write those records into the output memory. During this cycle new + orders can not be created, since otherwise the could be cancelled at + the second cycle without spending real time at the order book. + */ + tradingEngineModule.setCurrentCycle('First') + await runCycle() - /* Run the Manage Stage */ - tradingSystemModule.manageStage() - - /* Run the Close Stage */ - await tradingSystemModule.closeStage() + /* + We check if we need to stop before appending the records so that the stop + reason is also propery recorded. Note also that we check this after the first + cycle, where orders have not been submitted to the exchange yet, but we + had the chance to check for the status of placed orders or even cancel + the ones that needed cancellation. + */ + checkIfWeNeedToStopBetweenCycles() /* Add new records to the process output */ tradingRecordsModule.appendRecords() - controlLoop() - } + if (breakLoop === true) { break } + /* + Run the second cycle of the Trading System. During this second run + some new orders might be created at slots freed up during the first + run. This allows for example for a Limit Order to be cancelled during the + first run, and the same Limit Order definition to spawn a new order + without the need to wait until the next candle. Orders can not be cancelled + during the second cycle. + */ + tradingEngineModule.setCurrentCycle('Second') + await runCycle() - function controlLoop() { - /* 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 this session.') } - console.log('[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing this session.') - afterLoop() - return + checkIfWeNeedToStopAfterBothCycles() + + /* Add new records to the process output */ + tradingRecordsModule.appendRecords() + + if (breakLoop === true) { break } + + async function runCycle() { + /* Reset Data Structures */ + tradingSystemModule.reset() + tradingEpisodeModule.reset() + tradingEngineModule.reset() + + let infoMessage = 'Processing candle # ' + tradingEngine.current.episode.candle.index.value + ' @ the ' + tradingEngine.current.episode.cycle.value + ' cycle.' + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> ' + infoMessage) } + tradingSystem.infos.push([tradingSystem.id, infoMessage]) + + await tradingSystemModule.run() } - if (global.STOP_TASK_GRACEFULLY === 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 this task.') } - console.log('[INFO] runSimulation -> controlLoop -> We are going to stop here bacause we were requested to stop processing this task.') - afterLoop() - return + function checkIfWeNeedToStopBetweenCycles() { + 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 this session.') } + updateEpisode('Session Stopped') + breakLoop = true + return + } + + if (global.STOP_TASK_GRACEFULLY === 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 this task.') } + updateEpisode('Task Stopped') + breakLoop = true + return + } + + if (checkFinalDatetime() === false) { + closeEpisode('Final Datetime Reached') + breakLoop = true + bot.SESSION.stop('Final Datetime Reached') + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] runSimulation -> Final Datetime Reached. Stopping the Session now. ' ) } + return + } + + if (checkMinimunAndMaximunBalance() === false) { + closeEpisode('Min or Max Balance Reached') + breakLoop = true + bot.SESSION.stop('Min or Max Balance Reached') + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[IMPORTANT] runSimulation -> Min or Max Balance Reached. Stopping the Session now. ' ) } + return + } } - tradingEngine.current.candle.index.value++ - if (tradingEngine.current.candle.index.value < 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 checkIfWeNeedToStopAfterBothCycles() { + if (checkNextCandle() === false) { + updateEpisode('All Candles Processed') + breakLoop = true + return + } } } - function afterLoop() { - tradingEngineModule.finalize() - tradingSystemModule.finalize() - tradingRecordsModule.finalize() + tradingSystemModule.finalize() + tradingRecordsModule.finalize() + tradingEpisodeModule.finalize() - tradingEngineModule = undefined - tradingSystemModule = undefined - tradingRecordsModule = undefined + tradingSystemModule = undefined + tradingRecordsModule = undefined + tradingEpisodeModule = undefined - callback() + await writeFiles() + + function closeEpisode(exitType) { + tradingEpisodeModule.updateExitType(exitType) + tradingEpisodeModule.closeEpisode() + } + + function updateEpisode(exitType) { + tradingEpisodeModule.updateExitType(exitType) } function heartBeat() { - /* We will produce a simulation level heartbeat in order to inform the user this is running. */ - heartBeatDate = new Date(Math.trunc(tradingEngine.current.candle.begin.value / global.ONE_DAY_IN_MILISECONDS) * global.ONE_DAY_IN_MILISECONDS) - if (heartBeatDate.valueOf() !== previousHeartBeatDate) { - let processingDate = heartBeatDate.getUTCFullYear() + '-' + utilities.pad(heartBeatDate.getUTCMonth() + 1, 2) + '-' + utilities.pad(heartBeatDate.getUTCDate(), 2) + let hartbeatText = '' + if (sessionParameters.heartbeats !== undefined) { + if (sessionParameters.heartbeats.config.date === true || sessionParameters.heartbeats.config.candleIndex === true) { + /* We will produce a simulation level heartbeat in order to inform the user this is running. */ - if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> loop -> Simulation ' + bot.sessionKey + ' Loop # ' + tradingEngine.current.candle.index.value + ' @ ' + processingDate) } + heartBeatDate = new Date(Math.trunc(tradingEngine.current.episode.candle.begin.value / global.ONE_DAY_IN_MILISECONDS) * global.ONE_DAY_IN_MILISECONDS) - /* Telling the world we are alive and doing well */ - let fromDate = new Date(sessionParameters.timeRange.config.initialDatetime) - let lastDate = new Date(sessionParameters.timeRange.config.finalDatetime) + let fromDate = new Date(sessionParameters.timeRange.config.initialDatetime) + let lastDate = new Date(sessionParameters.timeRange.config.finalDatetime) - 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) + 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) - if (global.areEqualDates(currentDate, new Date()) === false) { - logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage) + /* + Theere are a few tasks that we need to do only when the date changes, + otherwise it would be suboptimal. + */ + if (heartBeatDate.valueOf() !== previousHeartBeatDate) { + previousHeartBeatDate = heartBeatDate.valueOf() + + 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.episode.candle.index.value + ' @ ' + processingDate) } + + /* Logging to console and disk */ + if (global.areEqualDates(currentDate, new Date()) === false) { + logger.newInternalLoop(bot.codeName, bot.process, currentDate, percentage) + } + + /* Date only hearbeat */ + if (sessionParameters.heartbeats.config.date === true && sessionParameters.heartbeats.config.candleIndex === false) { + hartbeatText = hartbeatText + currentDateString + bot.processHeartBeat(hartbeatText, percentage) + return + } + } + + /* + When the Candle Index nees to be shown, then we can not send the hearbet + only when the dates changes, we have to send it for every candle. + It might also contain the date information. + */ + if (sessionParameters.heartbeats.config.candleIndex === true ) { + if (sessionParameters.heartbeats.config.date === true ) { + hartbeatText = hartbeatText + currentDateString + } + hartbeatText = hartbeatText + ' Candle # ' + tradingEngine.current.episode.candle.index.value + bot.processHeartBeat(hartbeatText, percentage) + } } } - previousHeartBeatDate = heartBeatDate.valueOf() } function positionChartAtCurrentCandle() { @@ -226,7 +316,8 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT 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. */ - let dataDependencies = bot.processNode.referenceParent.processDependencies.dataDependencies + let dataDependencies = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processDependencies, 'Data Dependency') + dataDependencies = global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY(dataDependencies) /* Finding the Current Element on Market Files */ if (bot.processingDailyFiles) { @@ -240,8 +331,12 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT 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], 'Daily' + '-' + mapKey + '-' + pluralVariableName) - thisChart[singularVariableName] = currentElement + if (thisChart[pluralVariableName] !== undefined) { + let currentElement = getElement(thisChart[pluralVariableName], 'Daily' + '-' + mapKey + '-' + pluralVariableName) + if (currentElement !== undefined) { + thisChart[singularVariableName] = currentElement + } + } } } } @@ -257,8 +352,12 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT 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], 'Market' + '-' + mapKey + '-' + pluralVariableName) - thisChart[singularVariableName] = currentElement + if (thisChart[pluralVariableName] !== undefined) { + let currentElement = getElement(thisChart[pluralVariableName], 'Market' + '-' + mapKey + '-' + pluralVariableName) + if (currentElement !== undefined) { + thisChart[singularVariableName] = currentElement + } + } } } @@ -287,15 +386,15 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT for (let i = 0; i < pArray.length; i++) { element = pArray[i] - if (tradingEngine.current.candle.end.value === element.end) { // when there is an exact match at the end we take that element + if (tradingEngine.current.episode.candle.end.value === element.end) { // when there is an exact match at the end we take that element return element } else { if ( i > 0 && - element.end > tradingEngine.current.candle.end.value + element.end > tradingEngine.current.episode.candle.end.value ) { let previousElement = pArray[i - 1] - if (previousElement.end < tradingEngine.current.candle.end.value) { + if (previousElement.end < tradingEngine.current.episode.candle.end.value) { return previousElement // If one elements goes into the future of currentCandle, then we stop and take the previous element. } else { return @@ -307,43 +406,75 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT } catch (err) { logger.write(MODULE_NAME, '[ERROR] runSimulation -> getElement -> datasetName = ' + datasetName) logger.write(MODULE_NAME, '[ERROR] runSimulation -> getElement -> err = ' + err.stack) - callBackFunction(global.DEFAULT_FAIL_RESPONSE) + throw (global.DEFAULT_FAIL_RESPONSE) } } - function checkIfWeAreAtTheHeadOfTheMarket() { + function checkNextCandle() { /* - 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 + We need to check that the candle we have just processed it is not the last candle. + The candle at the head of the market is already skipped from the loop because it has not closed yet. + Note: for Daily Files, this means that the last candle of each day will never be processed. + + The first +1 is because array indexes are based on 0. + The second +1 is because we need to compare the next candle (remember that the loops allways avoid the + last candle of the dataset available.) */ - if (bot.processingDailyFiles) { // We are processing Daily Files + if (tradingEngine.current.episode.candle.index.value + 1 + 1 === candles.length ) { + /* + When processing daily files, we need a mechanism to turn from one day to the next one. + That mechanism is the one implemented here. If we detect that the next candle is the last candle of + the day, we will advance current process day one day. By doing so, during the next execution, the + simulation will receive the candles and indicators files of the next day. + */ let candlesPerDay = global.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.') } + if ( + bot.processingDailyFiles && + tradingEngine.current.episode.candle.index.value + 1 + 1 === candlesPerDay + ) { + /* + Here we found that the next candle of the dataset is the last candle of the day. + It is time to move to the next day so as to receive at the next execution, the indicator files from + the next day. At the same time we will reset the index to be pointing to + the first candle of the new dataset we shall receive. The first candle of the next day starts + at index 0, so we will position the index now at zero. + */ + tradingEngine.current.episode.candle.index.value = 0 + tradingEngine.current.episode.processDate.value = + tradingEngine.current.episode.processDate.value + global.ONE_DAY_IN_MILISECONDS return false } + + /* + We reached the head of the market but we are not at the last candle of a day during + Daily Files processing. We will advance to the next candle index anyways because in the + next execution it will likely have more candles at the dataset. And if it does not, + it will just wait there until it does. + */ + tradingEngine.current.episode.headOfTheMarket.value = true + tradingEngine.current.episode.candle.index.value++ + return false + } else { + + /* Wd did not reach the head of the market */ + tradingEngine.current.episode.headOfTheMarket.value = false + tradingEngine.current.episode.candle.index.value++ + return true + } + } + + function checkInitialDatetime() { + /* Here we check that the current candle is not before the initial datetime defined at the session parameters.*/ + if (tradingEngine.current.episode.candle.end.value < sessionParameters.timeRange.config.initialDatetime) { + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle before the sessionParameters.timeRange.config.initialDatetime.') } + 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 (tradingEngine.current.candle.end.value < sessionParameters.timeRange.config.initialDatetime) { - if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle before the sessionParameters.timeRange.config.initialDatetime.') } - return false - } - if (tradingEngine.current.candle.begin.value > sessionParameters.timeRange.config.finalDatetime) { + function checkFinalDatetime() { + /* Here we check that the next candle is not after of the user-defined final datetime at the session parameters.*/ + if (tradingEngine.current.episode.candle.begin.value + sessionParameters.timeFrame.config.value > sessionParameters.timeRange.config.finalDatetime) { if (FULL_LOG === true) { logger.write(MODULE_NAME, '[INFO] runSimulation -> checkInitialAndFinalDatetime -> Skipping Candle after the sessionParameters.timeRange.config.finalDatetime.') } return false } @@ -355,42 +486,55 @@ exports.newTradingSimulation = function newTradingSimulation(bot, logger, UTILIT if ( tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue ) { - /* - 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. + /* + We will perform this check only when we are not inside a position, + because there the balances have shifted from their resting position. */ - let minimumBalance - let maximumBalance - let balance - if (bot.sessionAndMarketBaseAssetsAreEqual) { - 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(tradingEngine.current.episode.candle.begin.value)).toLocaleString() + + if (sessionParameters.sessionBaseAsset.config.minimumBalance !== undefined) { + if (tradingEngine.current.episode.episodeBaseAsset.balance.value <= sessionParameters.sessionBaseAsset.config.minimumBalance) { + const errorMessage = 'Min Balance reached @ ' + stopRunningDate + tradingSystem.errors.push([tradingSystem.id, errorMessage]) + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) } + return false + } } - let stopRunningDate = new Date(tradingEngine.current.candle.begin.value) - 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 (sessionParameters.sessionBaseAsset.config.maximumBalance !== undefined) { + if (tradingEngine.current.episode.episodeBaseAsset.balance.value >= sessionParameters.sessionBaseAsset.config.maximumBalance) { + const errorMessage = 'Max Balance reached @ ' + stopRunningDate + tradingSystem.errors.push([tradingSystem.id, errorMessage]) + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) } + 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 + if (sessionParameters.sessionQuotedAsset.config.minimumBalance !== undefined) { + if (tradingEngine.current.episode.episodeQuotedAsset.balance.value <= sessionParameters.sessionQuotedAsset.config.minimumBalance) { + const errorMessage = 'Min Balance reached @ ' + stopRunningDate + tradingSystem.errors.push([tradingSystem.id, errorMessage]) + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) } + return false + } } + + if (sessionParameters.sessionQuotedAsset.config.maximumBalance !== undefined) { + if (tradingEngine.current.episode.episodeQuotedAsset.balance.value >= sessionParameters.sessionQuotedAsset.config.maximumBalance) { + const errorMessage = 'Max Balance reached @ ' + stopRunningDate + tradingSystem.errors.push([tradingSystem.id, errorMessage]) + if (FULL_LOG === true) { logger.write(MODULE_NAME, '[WARN] runSimulation -> checkMinimunAndMaximunBalance -> ' + errorMessage) } + return false + } + } + } return true } } catch (err) { logger.write(MODULE_NAME, '[ERROR] runSimulation -> err = ' + err.stack) - callBackFunction(global.DEFAULT_FAIL_RESPONSE) + throw (global.DEFAULT_FAIL_RESPONSE) } } } diff --git a/LowFrequencyTrading/TradingStages.js b/LowFrequencyTrading/TradingStages.js new file mode 100644 index 000000000..b3465bf18 --- /dev/null +++ b/LowFrequencyTrading/TradingStages.js @@ -0,0 +1,1055 @@ +exports.newTradingStages = function newTradingStages(bot, logger, tradingEngineModule) { + /* + This module packages all functions related to Stages. + */ + const MODULE_NAME = 'Trading Stages' + let thisObject = { + updateChart: updateChart, + mantain: mantain, + reset: reset, + cycleBasedStatistics: cycleBasedStatistics, + runTriggerStage: runTriggerStage, + runOpenStage: runOpenStage, + runManageStage: runManageStage, + runCloseStage: runCloseStage, + updateEnds: updateEnds, + resetTradingEngineDataStructure: resetTradingEngineDataStructure, + updateCounters: updateCounters, + updateStatistics: updateStatistics, + initialize: initialize, + finalize: finalize + } + + const TRADING_STRATEGY_MODULE = require('./TradingStrategy.js') + let tradingStrategyModule = TRADING_STRATEGY_MODULE.newTradingStrategy(bot, logger, tradingEngineModule) + + const TRADING_POSITION_MODULE = require('./TradingPosition.js') + let tradingPositionModule = TRADING_POSITION_MODULE.newTradingPosition(bot, logger, tradingEngineModule) + + const TRADING_ENGINE_EXECUTION = require('./TradingExecution.js') + let tradingExecutionModule = TRADING_ENGINE_EXECUTION.newTradingExecution(bot, logger, tradingEngineModule) + + const ANNOUNCEMENTS_MODULE = require('./Announcements.js') + let announcementsModule = ANNOUNCEMENTS_MODULE.newAnnouncements(bot, logger) + + const SNAPSHOTS_MODULE = require('./Snapshots.js') + let snapshotsModule = SNAPSHOTS_MODULE.newSnapshots(bot, logger) + + const TRADING_EPISODE_MODULE = require('./TradingEpisode.js') + let tradingEpisodeModule = TRADING_EPISODE_MODULE.newTradingEpisode(bot, logger) + + let tradingEngine + let tradingSystem + let sessionParameters + + return thisObject + + function initialize() { + tradingSystem = bot.simulationState.tradingSystem + tradingEngine = bot.simulationState.tradingEngine + sessionParameters = bot.SESSION.parameters + + tradingStrategyModule.initialize() + tradingPositionModule.initialize() + announcementsModule.initialize() + snapshotsModule.initialize() + tradingExecutionModule.initialize() + tradingEpisodeModule.initialize() + } + + function finalize() { + tradingEngine = undefined + tradingSystem = undefined + sessionParameters = undefined + + tradingStrategyModule.finalize() + tradingStrategyModule = undefined + + tradingPositionModule.finalize() + tradingPositionModule = undefined + + announcementsModule.finalize() + announcementsModule = undefined + + snapshotsModule.finalize() + snapshotsModule = undefined + + tradingExecutionModule.finalize() + tradingExecutionModule = undefined + + tradingEpisodeModule.finalize() + tradingEpisodeModule = undefined + } + + function updateChart(pChart) { + snapshotsModule.updateChart(pChart) + } + + function mantain() { + tradingPositionModule.mantain() + tradingStrategyModule.mantain() + tradingExecutionModule.mantain() + + updateCounters() + updateEnds() + } + + function reset() { + tradingPositionModule.reset() + tradingStrategyModule.reset() + tradingExecutionModule.reset() + + resetTradingEngineDataStructure() + } + + function cycleBasedStatistics() { + tradingEpisodeModule.cycleBasedStatistics() + tradingPositionModule.cycleBasedStatistics() + } + + function runTriggerStage() { + /* + We check if we will be triggering on, off or taking position. + */ + checkTriggerOn() + checkTriggerOff() + checkTakePosition() + + function checkTriggerOn() { + if ( + tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue + ) { + /* + To pick a new strategy we will check that any of the situations of the trigger on is true. + 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 conditions to enter all over again. + */ + tradingSystem.evalConditions(tradingSystem, 'Trigger On Event') + + for (let j = 0; j < tradingSystem.tradingStrategies.length; j++) { + if ( // If a strategy was already picked during the loop, we exit the loop + tradingEngine.current.strategy.index.value !== tradingEngine.current.strategy.index.config.initialValue + ) { continue } + + let strategy = tradingSystem.tradingStrategies[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 + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(triggerStage.triggerOn.id) + tradingSystem.highlights.push(triggerStage.id) + + tradingStrategyModule.openStrategy(j, situation.name, strategy.name) + + /* Initialize this */ + tradingEngine.current.episode.distanceToEvent.triggerOn.value = 1 + + announcementsModule.makeAnnoucements(triggerStage.triggerOn) + announcementsModule.makeAnnoucements(triggerStage) + + if (bot.SESSION.type === 'Backtesting Session') { + if (sessionParameters.snapshots !== undefined) { + if (sessionParameters.snapshots.config.strategy === true) { + snapshotsModule.strategyEntry() + } + } + } + + changeStageStatus('Trigger Stage', 'Open') + } + } + } + } + } + } + } + + function checkTriggerOff() { + if (tradingEngine.current.strategyTriggerStage.status.value === 'Open') { + + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let triggerStage = strategy.triggerStage + + tradingSystem.evalConditions(strategy, 'Trigger Off Event') + + 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 + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(triggerStage.triggerOff.id) + tradingSystem.highlights.push(triggerStage.id) + + tradingEngine.current.episode.distanceToEvent.triggerOff.value = 1 + announcementsModule.makeAnnoucements(triggerStage.triggerOff) + changeStageStatus('Trigger Stage', 'Closed') + tradingStrategyModule.closeStrategy('Trigger Off') + } + } + } + } + } + } + + function checkTakePosition() { + if ( + tradingEngine.current.strategyTriggerStage.status.value === 'Open' + ) { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let triggerStage = strategy.triggerStage + + tradingSystem.evalConditions(strategy, 'Take Position Event') + tradingSystem.evalFormulas(strategy, 'Take Position Event') + + 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 + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(triggerStage.takePosition.id) + tradingSystem.highlights.push(triggerStage.id) + + tradingPositionModule.openPosition(situation.name) + + announcementsModule.makeAnnoucements(triggerStage.takePosition) + announcementsModule.makeAnnoucements(strategy.openStage) + + if (bot.SESSION.type === 'Backtesting Session') { + if (sessionParameters.snapshots !== undefined) { + if (sessionParameters.snapshots.config.position === true) { + snapshotsModule.positionEntry() + } + } + } + + changeStageStatus('Trigger Stage', 'Closed') + changeStageStatus('Open Stage', 'Opening') + changeStageStatus('Manage Stage', 'Opening') + } + } + } + } + } + } + } + + async function runOpenStage() { + + checkIfWeNeedToAbortTheStage() + runWhenStatusIsOpening() + await runWhenStatusIsOpen() + await runWhenStatusIsClosing() + + function checkIfWeNeedToAbortTheStage() { + /* Abort Open Stage Check */ + if (tradingEngine.current.strategyOpenStage.status.value === 'Open' && tradingEngine.current.strategyCloseStage.status.value === 'Open') { + /* + if the Close stage is opened while the open stage is still open that means that + we need to stop placing orders, check what happened to the orders already placed, + and cancel all unfilled limit orders. We call this the Closing status of the Open Stage. + */ + changeStageStatus('Open Stage', 'Closing') + } + } + + function runWhenStatusIsOpening() { + /* Opening Status Procedure */ + if (tradingEngine.current.strategyOpenStage.status.value === 'Opening') { + + /* This procedure is intended to run only once */ + let tradingSystemStage = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage + let tradingEngineStage = tradingEngine.current.strategyOpenStage + + /* Reset the Exchange Orders data structure to its initial value */ + tradingEngineModule.initializeNode(tradingEngine.exchangeOrders) + + /* Entry Position size and rate */ + tradingSystem.evalFormulas(tradingSystemStage, 'Initial Targets') + tradingPositionModule.initialTargets(tradingSystemStage, tradingEngineStage) + initializeStageTargetSize() + + /* From here on, the stage is officialy Open */ + changeStageStatus('Open Stage', 'Open') + } + } + + async function runWhenStatusIsOpen() { + /* Open Status Procedure */ + if (tradingEngine.current.strategyOpenStage.status.value === 'Open') { + /* + While the Open Stage is Open, we do our regular stuff: place orders and check + what happened to the orders already placed. + */ + let tradingSystemStage = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage + let tradingEngineStage = tradingEngine.current.strategyOpenStage + let executionNode = tradingSystemStage.openExecution + + /* Evaluate conditions and formulas so they are ready during the execution run */ + tradingSystem.evalConditions(executionNode, 'Open Execution') + tradingSystem.evalFormulas(executionNode, 'Open Execution') + + await tradingExecutionModule.runExecution( + executionNode, + tradingEngineStage + ) + checkIfStageNeedsToBeClosed(tradingEngineStage, tradingSystemStage, 'Open Stage') + } + } + + async function runWhenStatusIsClosing() { + /* Closing Status Procedure */ + if (tradingEngine.current.strategyOpenStage.status.value === 'Closing') { + /* + During the closing stage status, we do not place new orders, just check + if the ones placed were filled, and cancel the ones not filled. + */ + let tradingSystemStage = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage + let executionNode = tradingSystemStage.openExecution + + /* + Check if there are unfilled orders, we will check if they were executed, + and cancel the ones that were not. + */ + tradingSystem.evalConditions(tradingSystemStage, 'Open Execution') + tradingSystem.evalFormulas(tradingSystemStage, 'Open Execution') + + await tradingExecutionModule.runExecution( + executionNode, + tradingEngine.current.strategyOpenStage + ) + + /* + The Closing is finished when the sizeFilled + feesPaid reaches the sizePlaced. + This can happens either because we update the sizeFilled value when we see + at the exchange that orders were filled, or we reduce the sizePlaced when + we cancel not yet filled orders. Here it does not matter the targetSize since + this status represent a force closure of this stage. + */ + switch (tradingEngine.current.strategyOpenStage.stageDefinedIn.value) { + case 'Base Asset': { + if ( + tradingEngine.current.strategyOpenStage.stageBaseAsset.sizeFilled.value >= + tradingEngine.current.strategyOpenStage.stageBaseAsset.sizePlaced.value + ) { + changeStageStatus('Open Stage', 'Closed') + } + break + } + case 'Quoted Asset': { + if ( + tradingEngine.current.strategyOpenStage.stageQuotedAsset.sizeFilled.value >= + tradingEngine.current.strategyOpenStage.stageQuotedAsset.sizePlaced.value + ) { + changeStageStatus('Open Stage', 'Closed') + } + break + } + } + } + } + + function initializeStageTargetSize() { + /* + Here we will transform the position size into targets for each asset of the stage. + */ + tradingEngine.current.strategyOpenStage.stageBaseAsset.targetSize.value = tradingEngine.current.position.positionBaseAsset.entryTargetSize.value + tradingEngine.current.strategyOpenStage.stageQuotedAsset.targetSize.value = tradingEngine.current.position.positionQuotedAsset.entryTargetSize.value + + tradingEngine.current.strategyOpenStage.stageBaseAsset.targetSize.value = global.PRECISE(tradingEngine.current.strategyOpenStage.stageBaseAsset.targetSize.value, 10) + tradingEngine.current.strategyOpenStage.stageQuotedAsset.targetSize.value = global.PRECISE(tradingEngine.current.strategyOpenStage.stageQuotedAsset.targetSize.value, 10) + } + } + + function runManageStage() { + + runWhenStatusIsOpening() + runWhenStatusIsOpen() + + function runWhenStatusIsOpening() { + /* Opening Status Procedure */ + if (tradingEngine.current.strategyManageStage.status.value === 'Opening') { + + /* Now we switch to the Open status. */ + changeStageStatus('Manage Stage', 'Open') + } + } + + function runWhenStatusIsOpen() { + /* Open Status Procedure */ + if (tradingEngine.current.strategyManageStage.status.value === 'Open') { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let manageStage = strategy.manageStage + + /* Evaluate all the stage conditions and formulas to have them ready */ + tradingSystem.evalConditions(manageStage, 'Manage Stage') + tradingSystem.evalFormulas(manageStage, 'Manage Stage') + + /* Stop Loss Management */ + checkStopPhasesEvents() + calculateStopLoss() + calculateStopLossPosition() + + /* Take Profit Management */ + checkTakeProfitPhaseEvents() + calculateTakeProfit() + calculateTakeProfitPosition() + + /* Checking if Stop or Take Profit were hit */ + checkStopLossOrTakeProfitWasHit() + } + } + + function calculateStopLoss() { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let manageStage = strategy.manageStage + let phase + + if (manageStage !== undefined) { + if (manageStage.managedStopLoss !== undefined) { + phase = manageStage.managedStopLoss.phases[tradingEngine.current.position.stopLoss.stopLossPhase.value - 1] + } + } + + if (phase !== undefined) { + if (phase.formula !== undefined) { + let previousValue = tradingEngine.current.position.stopLoss.value + tradingPositionModule.applyStopLossFormula(tradingSystem.formulas, phase.formula.id) + + if (tradingEngine.current.position.stopLoss.value !== previousValue) { + announcementsModule.makeAnnoucements(phase) + } + } + } + } + + function calculateTakeProfit() { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let manageStage = strategy.manageStage + let phase + + if (manageStage !== undefined) { + if (manageStage.managedTakeProfit !== undefined) { + phase = manageStage.managedTakeProfit.phases[tradingEngine.current.position.takeProfit.takeProfitPhase.value - 1] + } + } + + if (phase !== undefined) { + if (phase.formula !== undefined) { + let previousValue = tradingEngine.current.position.takeProfit.value + tradingPositionModule.applyTakeProfitFormula(tradingSystem.formulas, phase.formula.id) + + if (tradingEngine.current.position.takeProfit.value !== previousValue) { + announcementsModule.makeAnnoucements(phase) + } + } + } + } + + function calculateStopLossPosition() { + /* + The position of the Stop Loss (above or below the price) is needed in order to know + later if the price hit the Stop Loss or not at every Simulation candle. When we get + the first values of the simulation of the Stop Loss we check if it is above or below + the Position Rate, and we assign the values Above or Below to it. + */ + if (tradingEngine.current.position.stopLoss.stopLossPosition.value === tradingEngine.current.position.stopLoss.stopLossPosition.config.initialValue) { + if (tradingEngine.current.position.stopLoss.value > tradingEngine.current.position.entryTargetRate.value) { + tradingEngine.current.position.stopLoss.stopLossPosition.value = 'Above' + } else { + tradingEngine.current.position.stopLoss.stopLossPosition.value = 'Below' + } + } + } + + function calculateTakeProfitPosition() { + /* + The position of the Take Profit (above or below the price) is needed in order to know + later if the price hit the Take Profit or not at every Simulation candle. When we get + the first values of the simulation of the Take Profits we check if it is above or below + the Position Rates, and we assign the values Above or Below to it. + */ + if (tradingEngine.current.position.takeProfit.takeProfitPosition.value === tradingEngine.current.position.takeProfit.takeProfitPosition.config.initialValue) { + if (tradingEngine.current.position.takeProfit.value > tradingEngine.current.position.entryTargetRate.value) { + tradingEngine.current.position.takeProfit.takeProfitPosition.value = 'Above' + } else { + tradingEngine.current.position.takeProfit.takeProfitPosition.value = 'Below' + } + } + } + + function checkStopPhasesEvents() { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let manageStage = strategy.manageStage + let parentNode + let phaseIndex + let phase + let stopLoss + + if (manageStage !== undefined) { + if (manageStage.managedStopLoss !== undefined) { + parentNode = manageStage + phaseIndex = tradingEngine.current.position.stopLoss.stopLossPhase.value - 1 + stopLoss = manageStage.managedStopLoss + phase = stopLoss.phases[phaseIndex] + } + } + + if (parentNode !== undefined) { + if (phase === undefined) { return } // trying to jump to a phase that does not exists. + + /* Check the next Phase Event. */ + let nextPhaseEvent = phase.nextPhaseEvent + if (nextPhaseEvent !== undefined) { + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + let situation = nextPhaseEvent.situations[k] + let passed + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(nextPhaseEvent.id) + tradingSystem.highlights.push(phase.id) + tradingSystem.highlights.push(stopLoss.id) + tradingSystem.highlights.push(parentNode.id) + tradingSystem.highlights.push(manageStage.id) + + tradingPositionModule.updateStopLoss(tradingEngine.current.position.stopLoss.stopLossPhase.value + 1) + + announcementsModule.makeAnnoucements(nextPhaseEvent) + + /* Reset this counter */ + tradingEngine.current.episode.distanceToEvent.nextPhase.value = 1 + return // only one event can pass at the time + } + } + } + + /* Check the Move to Phase Events. */ + for (let n = 0; n < phase.moveToPhaseEvents.length; n++) { + let moveToPhaseEvent = phase.moveToPhaseEvents[n] + if (moveToPhaseEvent !== undefined) { + for (let k = 0; k < moveToPhaseEvent.situations.length; k++) { + let situation = moveToPhaseEvent.situations[k] + let passed + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(moveToPhaseEvent.id) + tradingSystem.highlights.push(phase.id) + tradingSystem.highlights.push(stopLoss.id) + tradingSystem.highlights.push(parentNode.id) + tradingSystem.highlights.push(manageStage.id) + + let moveToPhase = moveToPhaseEvent.referenceParent + if (moveToPhase !== undefined) { + for (let q = 0; q < stopLoss.phases.length; q++) { + if (stopLoss.phases[q].id === moveToPhase.id) { + tradingPositionModule.updateStopLoss(q + 1) + } + } + } else { + tradingSystem.errors.push([moveToPhaseEvent.id, 'This Node needs to reference a Phase.']) + continue + } + + announcementsModule.makeAnnoucements(moveToPhaseEvent) + + /* Reset this counter */ + tradingEngine.current.episode.distanceToEvent.moveToPhase.value = 1 + return // only one event can pass at the time + } + } + } + } + } + } + + function checkTakeProfitPhaseEvents() { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + let openStage = strategy.openStage + let manageStage = strategy.manageStage + let parentNode + let phaseIndex + let phase + let takeProfit + + if (manageStage !== undefined) { + if (manageStage.managedTakeProfit !== undefined) { + parentNode = manageStage + phaseIndex = tradingEngine.current.position.takeProfit.takeProfitPhase.value - 1 + takeProfit = manageStage.managedTakeProfit + phase = takeProfit.phases[phaseIndex] + } + } + + if (parentNode !== undefined) { + if (phase === undefined) { return } // trying to jump to a phase that does not exists. + + /* Check the next Phase Event. */ + let nextPhaseEvent = phase.nextPhaseEvent + if (nextPhaseEvent !== undefined) { + for (let k = 0; k < nextPhaseEvent.situations.length; k++) { + let situation = nextPhaseEvent.situations[k] + let passed + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(nextPhaseEvent.id) + tradingSystem.highlights.push(phase.id) + tradingSystem.highlights.push(takeProfit.id) + tradingSystem.highlights.push(parentNode.id) + tradingSystem.highlights.push(manageStage.id) + + tradingPositionModule.updateTakeProfit(tradingEngine.current.position.takeProfit.takeProfitPhase.value + 1) + + announcementsModule.makeAnnoucements(nextPhaseEvent) + + /* Reset this counter */ + tradingEngine.current.episode.distanceToEvent.nextPhase.value = 1 + return // only one event can pass at the time + } + } + } + + /* Check the Move to Phase Events. */ + for (let n = 0; n < phase.moveToPhaseEvents.length; n++) { + let moveToPhaseEvent = phase.moveToPhaseEvents[n] + if (moveToPhaseEvent !== undefined) { + for (let k = 0; k < moveToPhaseEvent.situations.length; k++) { + let situation = moveToPhaseEvent.situations[k] + let passed + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(moveToPhaseEvent.id) + tradingSystem.highlights.push(phase.id) + tradingSystem.highlights.push(takeProfit.id) + tradingSystem.highlights.push(parentNode.id) + tradingSystem.highlights.push(manageStage.id) + + let moveToPhase = moveToPhaseEvent.referenceParent + if (moveToPhase !== undefined) { + for (let q = 0; q < takeProfit.phases.length; q++) { + if (takeProfit.phases[q].id === moveToPhase.id) { + tradingPositionModule.updateTakeProfit(q + 1) + } + } + } else { + tradingSystem.errors.push([moveToPhaseEvent.id, 'This Node needs to reference a Phase.']) + continue + } + + announcementsModule.makeAnnoucements(moveToPhaseEvent) + + /* Reset this counter */ + tradingEngine.current.episode.distanceToEvent.moveToPhase.value = 1 + return // only one event can pass at the time + } + } + } + } + } + } + + function checkStopLossOrTakeProfitWasHit() { + { + let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] + /* + 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 ( + ( + tradingEngine.current.position.stopLoss.stopLossPosition.value === 'Above' && + tradingEngine.current.episode.candle.max.value >= tradingEngine.current.position.stopLoss.value + ) || + ( + tradingEngine.current.position.stopLoss.stopLossPosition.value === 'Below' && + tradingEngine.current.episode.candle.min.value <= tradingEngine.current.position.stopLoss.value + ) + ) { + logger.write(MODULE_NAME, '[INFO] checkStopLossOrTakeProfitWasHit -> Stop Loss was hit.') + + tradingPositionModule.closingPosition('Stop Loss') + changeStageStatus('Close Stage', 'Opening') + changeStageStatus('Manage Stage', 'Closed') + announcementsModule.makeAnnoucements(strategy.closeStage) + return + } + + /* Take Profit condition: Here we verify if the Take Profit was hit or not. */ + if ( + ( + tradingEngine.current.position.takeProfit.takeProfitPosition.value === 'Below' && + tradingEngine.current.episode.candle.min.value <= tradingEngine.current.position.takeProfit.value + ) || + ( + tradingEngine.current.position.takeProfit.takeProfitPosition.value === 'Above' && + tradingEngine.current.episode.candle.max.value >= tradingEngine.current.position.takeProfit.value + ) + ) { + logger.write(MODULE_NAME, '[INFO] checkStopLossOrTakeProfitWasHit -> Take Profit was hit.') + + tradingPositionModule.closingPosition('Take Profit') + changeStageStatus('Close Stage', 'Opening') + changeStageStatus('Manage Stage', 'Closed') + announcementsModule.makeAnnoucements(strategy.closeStage) + return + } + return + } + } + } + + async function runCloseStage() { + + runWhenStatusIsOpening() + await runWhenStatusIsOpen() + + function runWhenStatusIsOpening() { + /* Opening Status Procedure */ + if (tradingEngine.current.strategyCloseStage.status.value === 'Opening') { + /* + This will happen only once, as soon as the Take Profit or Stop was hit. + */ + let tradingSystemStage = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].closeStage + + /* Exit Position size and rate */ + tradingSystem.evalFormulas(tradingSystemStage, 'Initial Targets') + tradingPositionModule.initialTargets(tradingSystemStage, tradingEngine.current.strategyCloseStage) + + initializeStageTargetSize() + changeStageStatus('Close Stage', 'Open') + } + } + + async function runWhenStatusIsOpen() { + /* + The following code will run only when the Open Stage is closed. + The reason for that is that we need the final filled size of the Open Stage + to be known before we start placing orders at the Close Stage. + */ + if ( + tradingEngine.current.strategyCloseStage.status.value === 'Open' && + ( + tradingEngine.current.strategyOpenStage.status.value === 'Closed' || + tradingEngine.current.strategyOpenStage.status.value == tradingEngine.current.strategyOpenStage.status.config.initialValue + ) + ) { + /* + This will happen as long as the Close Stage is Open. + */ + let tradingSystemStage = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].closeStage + let tradingEngineStage = tradingEngine.current.strategyCloseStage + let executionNode = tradingSystemStage.closeExecution + + tradingSystem.evalConditions(tradingSystemStage, 'Close Execution') + tradingSystem.evalFormulas(tradingSystemStage, 'Close Execution') + + await tradingExecutionModule.runExecution( + executionNode, + tradingEngine.current.strategyCloseStage + ) + + checkIfStageNeedsToBeClosed(tradingEngineStage, tradingSystemStage, 'Close Stage') + } + + /* + Exit Position Validation: we need al the previous stages to be either closed + or at their initial default value. The last is becasue after Closed objects are + initialized to their defualts. + */ + if ( + ( + tradingEngine.current.strategyTriggerStage.status.value === 'Closed' || + tradingEngine.current.strategyTriggerStage.status.value === tradingEngine.current.strategyTriggerStage.status.config.initialValue + ) + && + ( + tradingEngine.current.strategyOpenStage.status.value === 'Closed' || + tradingEngine.current.strategyOpenStage.status.value === tradingEngine.current.strategyOpenStage.status.config.initialValue + ) + && + ( + tradingEngine.current.strategyManageStage.status.value === 'Closed' || + tradingEngine.current.strategyManageStage.status.value === tradingEngine.current.strategyManageStage.status.config.initialValue + ) + && + ( + tradingEngine.current.strategyCloseStage.status.value === 'Closed' + ) + ) { + + /* Closing Everything now. */ + closePositionAndStrategy() + } + } + + function initializeStageTargetSize() { + /* + Here we will transform the position size into targets for each asset of the stage. + */ + tradingEngine.current.strategyCloseStage.stageBaseAsset.targetSize.value = tradingEngine.current.position.positionBaseAsset.exitTargetSize.value + tradingEngine.current.strategyCloseStage.stageQuotedAsset.targetSize.value = tradingEngine.current.position.positionQuotedAsset.exitTargetSize.value + + tradingEngine.current.strategyCloseStage.stageBaseAsset.targetSize.value = global.PRECISE(tradingEngine.current.strategyCloseStage.stageBaseAsset.targetSize.value, 10) + tradingEngine.current.strategyCloseStage.stageQuotedAsset.targetSize.value = global.PRECISE(tradingEngine.current.strategyCloseStage.stageQuotedAsset.targetSize.value, 10) + } + + function closePositionAndStrategy() { + /* Taking Position Snapshot */ + if (bot.SESSION.type === 'Backtesting Session') { + if (sessionParameters.snapshots !== undefined) { + if (sessionParameters.snapshots.config.position === true) { + snapshotsModule.positionExit() + } + } + } + + /* Taking Strategy Snapshot */ + if (bot.SESSION.type === 'Backtesting Session') { + if (sessionParameters.snapshots !== undefined) { + if (sessionParameters.snapshots.config.strategy === true) { + snapshotsModule.strategyExit() + } + } + } + + /* Close the Position */ + tradingPositionModule.closePosition() + + /* Close the Strategy */ + tradingStrategyModule.closeStrategy('Position Closed') + + /* Distance to Events Updates */ + tradingEngine.current.episode.distanceToEvent.closePosition.value = 1 + tradingEngine.current.episode.distanceToEvent.triggerOff.value = 1 + + } + } + + function checkIfStageNeedsToBeClosed(tradingEngineStage, tradingSystemStage, stageName) { + /* + The Stage is closed when the placedSize reaches the targetSize, and the fillSize + + feesPaid reaches the placedSize. This depends on how the user defined the stage: + either on Base Asset or Quoted Asset. + This can happens at any time when we update the sizeFilled and the feesPaid values + when we see at the exchange that orders were filled. + Note that the comparison is made with a Rounding Factor in order to avoid rounding problems. + */ + const ROUNDING_ERROR_CORRECTION_FACTOR = 1.001 + + switch (tradingEngineStage.stageDefinedIn.value) { + case 'Base Asset': { + if ( + tradingEngineStage.stageBaseAsset.sizeFilled.value * ROUNDING_ERROR_CORRECTION_FACTOR + >= + tradingEngineStage.stageBaseAsset.targetSize.value + ) { + positionFilled() + } else { + checkCloseStageEvent(tradingSystemStage) + } + break + } + case 'Quoted Asset': { + if ( + tradingEngineStage.stageQuotedAsset.sizeFilled.value * ROUNDING_ERROR_CORRECTION_FACTOR + >= + tradingEngineStage.stageQuotedAsset.targetSize.value + ) { + positionFilled() + } else { + checkCloseStageEvent(tradingSystemStage) + } + break + } + } + function positionFilled() { + changeStageStatus(stageName, 'Closed', 'Position Size Filled') + } + function checkCloseStageEvent(tradingSystemStage) { + /* Check the Close Stage Event */ + tradingSystem.evalConditions(tradingSystemStage, 'Close Stage Event') + if (checkStopStageEvent(tradingSystemStage) === true) { + changeStageStatus(stageName, 'Closing', 'Close Stage Event') + } + } + } + + function checkStopStageEvent(stage) { + /* Check the Close Stage Event. */ + let stopStageEvent = stage.stopStageEvent + if (stopStageEvent !== undefined) { + for (let k = 0; k < stopStageEvent.situations.length; k++) { + let situation = stopStageEvent.situations[k] + let passed + if (situation.conditions.length > 0) { + passed = true + } + + passed = tradingSystem.checkConditions(situation, passed) + + tradingSystem.values.push([situation.id, passed]) + if (passed) { + tradingSystem.highlights.push(situation.id) + tradingSystem.highlights.push(stopStageEvent.id) + tradingSystem.highlights.push(stage.id) + + announcementsModule.makeAnnoucements(stopStageEvent) + return true + } + } + } + } + + function changeStageStatus(stageName, newStatus, exitType) { + let stage + switch (stageName) { + case 'Trigger Stage': { + stage = tradingEngine.current.strategyTriggerStage + break + } + case 'Open Stage': { + stage = tradingEngine.current.strategyOpenStage + break + } + case 'Manage Stage': { + stage = tradingEngine.current.strategyManageStage + break + } + case 'Close Stage': { + stage = tradingEngine.current.strategyCloseStage + break + } + } + stage.status.value = newStatus + if (exitType !== undefined) { + stage.exitType.value = exitType + } + if (stage.status.value === 'Open') { + openStage(stage) + } + if (stage.status.value === 'Closed') { + closeStage(stage) + } + + function openStage(stage) { + /* Recording the opening at the Trading Engine Data Structure */ + stage.begin.value = tradingEngine.current.episode.cycle.begin.value + stage.beginRate.value = tradingEngine.current.episode.candle.close.value + stage.end.value = tradingEngine.current.episode.cycle.end.value + } + + function closeStage(stage) { + /* Recording the closing at the Trading Engine Data Structure */ + stage.end.value = tradingEngine.current.episode.cycle.end.value + stage.endRate.value = tradingEngine.current.episode.candle.close.value + } + } + + function updateEnds() { + if (tradingEngine.current.strategyTriggerStage.status.value === 'Open') { + tradingEngine.current.strategyTriggerStage.end.value = tradingEngine.current.episode.cycle.end.value + tradingEngine.current.strategyTriggerStage.endRate.value = tradingEngine.current.episode.candle.close.value + } + if (tradingEngine.current.strategyOpenStage.status.value === 'Open') { + tradingEngine.current.strategyOpenStage.end.value = tradingEngine.current.episode.cycle.end.value + tradingEngine.current.strategyOpenStage.endRate.value = tradingEngine.current.episode.candle.close.value + } + if (tradingEngine.current.strategyManageStage.status.value === 'Open') { + tradingEngine.current.strategyManageStage.end.value = tradingEngine.current.episode.cycle.end.value + tradingEngine.current.strategyManageStage.endRate.value = tradingEngine.current.episode.candle.close.value + } + if (tradingEngine.current.strategyCloseStage.status.value === 'Open') { + tradingEngine.current.strategyCloseStage.end.value = tradingEngine.current.episode.cycle.end.value + tradingEngine.current.strategyCloseStage.endRate.value = tradingEngine.current.episode.candle.close.value + } + } + + function resetTradingEngineDataStructure() { + if (tradingEngine.current.strategyTriggerStage.status.value === 'Closed') { + resetStage(tradingEngine.current.strategyTriggerStage.status) + } + if (tradingEngine.current.strategyOpenStage.status.value === 'Closed') { + resetStage(tradingEngine.current.strategyOpenStage.status) + } + if (tradingEngine.current.strategyManageStage.status.value === 'Closed') { + resetStage(tradingEngine.current.strategyManageStage.status) + } + if (tradingEngine.current.strategyCloseStage.status.value === 'Closed') { + resetStage(tradingEngine.current.strategyTriggerStage) + resetStage(tradingEngine.current.strategyOpenStage) + resetStage(tradingEngine.current.strategyManageStage) + resetStage(tradingEngine.current.strategyCloseStage) + } + + function resetStage(stage) { + tradingEngineModule.initializeNode(stage) + } + } + + function updateCounters() { + + } + + function updateStatistics() { + + } +} \ No newline at end of file diff --git a/LowFrequencyTrading/TradingStrategy.js b/LowFrequencyTrading/TradingStrategy.js index adcf0ca71..5225f705d 100644 --- a/LowFrequencyTrading/TradingStrategy.js +++ b/LowFrequencyTrading/TradingStrategy.js @@ -4,13 +4,10 @@ exports.newTradingStrategy = function newTradingStrategy(bot, logger, tradingEng */ const MODULE_NAME = 'Trading Strategy' let thisObject = { + mantain: mantain, + reset: reset, openStrategy: openStrategy, closeStrategy: closeStrategy, - updateEnds: updateEnds, - updateStageStatus: updateStageStatus, - updateStatus: updateStatus, - updateCounters: updateCounters, - resetStrategy: resetStrategy, initialize: initialize, finalize: finalize } @@ -27,66 +24,52 @@ exports.newTradingStrategy = function newTradingStrategy(bot, logger, tradingEng tradingEngine = undefined } + function mantain() { + updateCounters() + updateEnds() + } + + function reset() { + resetTradingEngineDataStructure() + } + function openStrategy(index, situationName, strategyName) { tradingEngine.current.strategy.status.value = 'Open' - tradingEngine.current.strategy.serialNumber.value = tradingEngine.episode.episodeCounters.strategies.value + tradingEngine.current.strategy.serialNumber.value = tradingEngine.current.episode.episodeCounters.strategies.value tradingEngine.current.strategy.identifier.value = global.UNIQUE_ID() - tradingEngine.current.strategy.begin.value = tradingEngine.current.candle.begin.value - tradingEngine.current.strategy.end.value = tradingEngine.current.candle.end.value - tradingEngine.current.strategy.beginRate.value = tradingEngine.current.candle.min.value - tradingEngine.current.strategy.endRate.value = tradingEngine.current.candle.min.value + tradingEngine.current.strategy.begin.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.strategy.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.strategy.beginRate.value = tradingEngine.current.episode.candle.min.value tradingEngine.current.strategy.index.value = index tradingEngine.current.strategy.situationName.value = situationName tradingEngine.current.strategy.strategyName.value = strategyName + + /* Updating Episode Counters */ + tradingEngine.current.episode.episodeCounters.strategies.value++ } function closeStrategy(exitType) { tradingEngine.current.strategy.status.value = 'Closed' tradingEngine.current.strategy.exitType.value = exitType - tradingEngine.current.strategy.end.value = tradingEngine.current.candle.end.value - tradingEngine.current.strategy.endRate.value = tradingEngine.current.candle.min.value + tradingEngine.current.strategy.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.strategy.endRate.value = tradingEngine.current.episode.candle.min.value /* Now that the strategy is closed, it is the right time to move this strategy from current to last at the Trading Engine data structure. */ tradingEngineModule.cloneValues(tradingEngine.current.strategy, tradingEngine.last.strategy) } - function resetStrategy() { - tradingEngine.current.strategy.initialize(tradingEngine.current.strategy) - } - function updateEnds() { if (tradingEngine.current.strategy.status.value === 'Open') { - tradingEngine.current.strategy.end.value = tradingEngine.current.candle.end.value - tradingEngine.current.strategy.endRate.value = tradingEngine.current.candle.min.value + tradingEngine.current.strategy.end.value = tradingEngine.current.episode.candle.end.value + tradingEngine.current.strategy.endRate.value = tradingEngine.current.episode.candle.close.value } } - function updateStageStatus(stage, status) { - switch (stage) { - case 'Trigger Stage': { - tradingEngine.current.strategy.triggerStageStatus.value = status - break - } - case 'Open Stage': { - tradingEngine.current.strategy.openStageStatus.value = status - break - } - case 'Manage Stage': { - tradingEngine.current.strategy.manageStageStatus.value = status - break - } - case 'Close Stage': { - tradingEngine.current.strategy.closeStageStatus.value = status - break - } - } - } - - function updateStatus() { + function resetTradingEngineDataStructure() { if (tradingEngine.current.strategy.status.value === 'Closed') { - resetStrategy() + tradingEngineModule.initializeNode(tradingEngine.current.strategy) } } diff --git a/LowFrequencyTrading/TradingSystem.js b/LowFrequencyTrading/TradingSystem.js index 7aafa99d0..d90c558ae 100644 --- a/LowFrequencyTrading/TradingSystem.js +++ b/LowFrequencyTrading/TradingSystem.js @@ -5,14 +5,10 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM */ const MODULE_NAME = 'Trading System' let thisObject = { + mantain: mantain, reset: reset, - mantainStrategies: mantainStrategies, - mantainPositions: mantainPositions, - mantainOrders: mantainOrders, - triggerStage: triggerStage, - openStage: openStage, - manageStage: manageStage, - closeStage: closeStage, + run: run, + updateChart: updateChart, initialize: initialize, finalize: finalize } @@ -21,21 +17,11 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM let tradingSystem let tradingEngine let sessionParameters + let dynamicIndicators - const TRADING_STRATEGY_MODULE = require('./TradingStrategy.js') - let tradingStrategyModule = TRADING_STRATEGY_MODULE.newTradingStrategy(bot, logger, tradingEngineModule) + const TRADING_STAGES_MODULE = require('./TradingStages.js') + let tradingStagesModule = TRADING_STAGES_MODULE.newTradingStages(bot, logger, tradingEngineModule) - const TRADING_POSITION_MODULE = require('./TradingPosition.js') - let tradingPositionModule = TRADING_POSITION_MODULE.newTradingPosition(bot, logger, tradingEngineModule) - - const TRADING_ENGINE_EXECUTION = require('./TradingExecution.js') - let tradingExecutionModule = TRADING_ENGINE_EXECUTION.newTradingExecution(bot, logger, tradingEngineModule) - - const ANNOUNCEMENTS_MODULE = require('./Announcements.js') - let announcementsModule = ANNOUNCEMENTS_MODULE.newAnnouncements(bot, logger) - - const SNAPSHOTS_MODULE = require('./Snapshots.js') - let snapshotsModule = SNAPSHOTS_MODULE.newSnapshots(bot, logger) return thisObject @@ -47,11 +33,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM tradingSystem.conditions = new Map() tradingSystem.formulas = new Map() - tradingStrategyModule.initialize() - tradingPositionModule.initialize() - announcementsModule.initialize() - snapshotsModule.initialize() - tradingExecutionModule.initialize() + tradingStagesModule.initialize() /* Adding Functions used elsewhere to Trading System Definition */ tradingSystem.checkConditions = function (situation, passed) { @@ -73,23 +55,31 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM } return passed } + + tradingSystem.evalConditions = function (startingNode, descendentOfNodeType) { + evalNode(startingNode, 'Conditions', descendentOfNodeType) + } + + tradingSystem.evalFormulas = function (startingNode, descendentOfNodeType) { + evalNode(startingNode, 'Formulas', descendentOfNodeType) + } + + function isItInside(elementWithTimestamp, elementWithBeginEnd) { + /* + The function is to allow in Conditions and Formulas to easily know when an element with timestamp (like the ones of single files) are inside the time range + of an element with a time range, like a candle. + */ + if (elementWithTimestamp.timestamp >= elementWithBeginEnd.begin && elementWithTimestamp.timestamp <= elementWithBeginEnd.end) { + return true + } else { + return false + } + } } function finalize() { - tradingStrategyModule.finalize() - tradingStrategyModule = undefined - - tradingPositionModule.finalize() - tradingPositionModule = undefined - - announcementsModule.finalize() - announcementsModule = undefined - - snapshotsModule.finalize() - snapshotsModule = undefined - - tradingExecutionModule.finalize() - tradingExecutionModule = undefined + tradingStagesModule.finalize() + tradingStagesModule = undefined chart = undefined @@ -98,6 +88,8 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM tradingSystem.highlights = undefined tradingSystem.errors = undefined + tradingSystem.warnings = undefined + tradingSystem.infos = undefined tradingSystem.values = undefined tradingSystem.status = undefined tradingSystem.progress = undefined @@ -108,13 +100,17 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM sessionParameters = undefined } - function reset(pChart) { - /* This function helps reset data structures at every cycle of the simulation loop/=. */ - chart = pChart // We need chat to be a local object accessible from conditions and formulas. - snapshotsModule.reset(chart) + function mantain() { + tradingStagesModule.mantain() + } + + function reset() { + tradingStagesModule.reset() tradingSystem.highlights = [] tradingSystem.errors = [] + tradingSystem.warnings = [] + tradingSystem.infos = [] tradingSystem.values = [] tradingSystem.status = [] tradingSystem.progress = [] @@ -122,68 +118,58 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM tradingSystem.announcements = [] } - function mantainStrategies() { - tradingStrategyModule.updateStatus() - tradingStrategyModule.updateCounters() - tradingStrategyModule.updateEnds() + function updateChart(pChart) { + chart = pChart // We need chat to be a local object accessible from conditions and formulas. + tradingStagesModule.updateChart(pChart) } - function mantainPositions() { - tradingPositionModule.updateStatus() - tradingPositionModule.updateCounters() - tradingPositionModule.updateEnds() - } + function buildDynamicIndicators() { + if (tradingSystem.dynamicIndicators !== undefined) { + dynamicIndicators = {} + /* Eval Dynamic Indicators */ + tradingSystem.evalFormulas(tradingSystem.dynamicIndicators, 'Indicator Function') - function mantainOrders() { - /* Reset All Exchange Orders that are on Closed status, to their initial value */ - if (tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue) { return } - - let stageNode - let executionNode - - stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage - executionNode = stageNode.openExecution - resetExecution(executionNode) - - stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].closeStage - executionNode = stageNode.closeExecution - resetExecution(executionNode) - - function resetExecution(executionNode) { - - resetExecutionAlgorithms(executionNode) - - function resetExecutionAlgorithms(executionNode) { - for (let i = 0; i < executionNode.executionAlgorithms.length; i++) { - let executionAlgorithm = executionNode.executionAlgorithms[i] - resetOrders(executionAlgorithm.marketBuyOrders, executionAlgorithm, executionNode) - resetOrders(executionAlgorithm.marketSellOrders, executionAlgorithm, executionNode) - resetOrders(executionAlgorithm.limitBuyOrders, executionAlgorithm, executionNode) - resetOrders(executionAlgorithm.limitSellOrders, executionAlgorithm, executionNode) - } - } - - function resetOrders(orders, executionAlgorithm, executionNode) { - for (let i = 0; i < orders.length; i++) { - let tradingSystemOrder = orders[i] - if (tradingSystemOrder.referenceParent === undefined) { continue } - let tradingEngineOrder = tradingEngineModule.getNodeById(tradingSystemOrder.referenceParent.id) - if (tradingEngineOrder.status === undefined) { continue } - if (tradingEngineOrder.status.value === 'Closed') { - /* We reset the order data structure inside the Trading Engine to its initial value */ - tradingEngineOrder.initialize(tradingEngineOrder) - } - } + for (let i = 0; i < tradingSystem.dynamicIndicators.indicatorFunctions.length; i++) { + let indicatorFunction = tradingSystem.dynamicIndicators.indicatorFunctions[i] + if (indicatorFunction.formula === undefined) { return} + if (indicatorFunction.config.codeName === undefined) { return} + dynamicIndicators[indicatorFunction.config.codeName] = tradingSystem.formulas.get(indicatorFunction.formula.id) } } } - function evalConditions(startingNode, descendentOfNodeType) { - evalNode(startingNode, 'Conditions', descendentOfNodeType) - } + async function run() { + try { + /* Dynamic Indicators */ + buildDynamicIndicators() - function evalFormulas(startingNode, descendentOfNodeType) { - evalNode(startingNode, 'Formulas', descendentOfNodeType) + /* Run the Trigger Stage */ + tradingStagesModule.runTriggerStage() + + /* Run the Open Stage */ + await tradingStagesModule.runOpenStage() + + /* Run the Manage Stage */ + tradingStagesModule.runManageStage() + + /* Run the Close Stage */ + await tradingStagesModule.runCloseStage() + + tradingStagesModule.cycleBasedStatistics() + + } catch (err) { + /* + If an error ocurred during execution, it was alreeady logged and + included at the errors array. That means we need to do nothing here, + just prevent the execution to be halted for not handling exceptions. + */ + if (typeof err === 'string' || err instanceof String) { + logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err) + } + if (err.stack !== undefined) { + logger.write(MODULE_NAME, '[ERROR] runExecution -> err = ' + err.stack) + } + } } function evalNode(node, evaluating, descendentOfNodeType, isDescendent) { @@ -205,7 +191,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM } /* Here we check if there is a formula to be evaluated */ - if (node.type === 'Formula' && evaluating === 'Formulas') { + if ((node.type === 'Formula' || node.type === 'Announcement Formula') && evaluating === 'Formulas') { if (node.code !== undefined) { /* We will eval this formula */ if (isDescendent === true) { @@ -215,7 +201,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM } /* Now we go down through all this node children */ - let nodeDefinition = bot.APP_SCHEMA_MAP.get(node.type) + let nodeDefinition = global.APP_SCHEMA_MAP.get(node.type) if (nodeDefinition === undefined) { return } if (nodeDefinition.properties !== undefined) { @@ -263,7 +249,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM } try { - logger.write(MODULE_NAME, '[INFO] evalCondition -> code = ' + code) + logger.write(MODULE_NAME, '[INFO] evalCondition -> ' + node.name + ' -> code = ' + code) value = eval(code) } catch (err) { /* @@ -272,8 +258,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM */ value = false - if (code.indexOf('previous') > -1 && err.message.indexOf('of undefined') > -1 || - code.indexOf('chart') > -1 && err.message.indexOf('of undefined') > -1 + if (code.indexOf('previous') > -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. @@ -304,7 +289,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM let error try { - logger.write(MODULE_NAME, '[INFO] evalFormula -> code = ' + node.code) + logger.write(MODULE_NAME, '[INFO] evalFormula -> ' + node.name + ' -> code = ' + node.code) value = eval(node.code) } catch (err) { /* @@ -313,8 +298,7 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM */ value = 0 - if (node.code.indexOf('previous') > -1 && err.message.indexOf('of undefined') > -1 || - node.code.indexOf('chart') > -1 && err.message.indexOf('of undefined') > -1 + if (node.code.indexOf('previous') > -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. @@ -324,786 +308,22 @@ exports.newTradingSystem = function newTradingSystem(bot, logger, tradingEngineM } } - tradingSystem.formulas.set(node.id, value) - if (error !== undefined) { tradingSystem.errors.push([node.id, error]) + return } if (value !== undefined) { + if (node.type === 'Formula' && isNaN(value)) { + tradingSystem.errors.push([node.id, 'Formula needs to return a number.']) + return + } + tradingSystem.values.push([node.id, value]) + tradingSystem.formulas.set(node.id, value) } logger.write(MODULE_NAME, '[INFO] evalFormula -> value = ' + value) logger.write(MODULE_NAME, '[INFO] evalFormula -> error = ' + error) } - - function triggerStage() { - /* - We check if we will be triggering on, off or taking position. - */ - checkTriggerOn() - checkTriggerOff() - checkTakePosition() - - function checkTriggerOn() { - if ( - tradingEngine.current.strategy.index.value === tradingEngine.current.strategy.index.config.initialValue - ) { - /* - To pick a new strategy we will check that any of the situations of the trigger on is true. - 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 conditions to enter all over again. - */ - evalConditions(tradingSystem, 'Trigger On Event') - - for (let j = 0; j < tradingSystem.tradingStrategies.length; j++) { - if ( // If a strategy was already picked during the loop, we exit the loop - tradingEngine.current.strategy.index.value !== tradingEngine.current.strategy.index.config.initialValue - ) { continue } - - let strategy = tradingSystem.tradingStrategies[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 - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(triggerStage.triggerOn.id) - tradingSystem.highlights.push(triggerStage.id) - - tradingStrategyModule.openStrategy(j, situation.name, strategy.name) - - /* Updating Episode Counters */ - tradingEngine.episode.episodeCounters.strategies.value++ - - /* Initialize this */ - tradingEngine.current.distanceToEvent.triggerOn.value = 1 - - announcementsModule.makeAnnoucements(triggerStage.triggerOn) - announcementsModule.makeAnnoucements(triggerStage) - - if (bot.SESSION.type === 'Backtesting Session') { - if (sessionParameters.snapshots !== undefined) { - if (sessionParameters.snapshots.config.strategy === true) { - snapshotsModule.strategyEntry() - } - } - } - - tradingStrategyModule.updateStageStatus('Trigger Stage', 'Open') - } - } - } - } - } - } - } - - function checkTriggerOff() { - if (tradingEngine.current.strategy.triggerStageStatus.value === 'Open') { - - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let triggerStage = strategy.triggerStage - - evalConditions(strategy, 'Trigger Off Event') - - 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 - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(triggerStage.triggerOff.id) - tradingSystem.highlights.push(triggerStage.id) - - tradingEngine.current.distanceToEvent.triggerOff.value = 1 - announcementsModule.makeAnnoucements(triggerStage.triggerOff) - tradingStrategyModule.updateStageStatus('Trigger Stage', 'Closed') - tradingStrategyModule.closeStrategy('Trigger Off') - } - } - } - } - } - } - - function checkTakePosition() { - if ( - tradingEngine.current.strategy.triggerStageStatus.value === 'Open' - ) { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let triggerStage = strategy.triggerStage - - evalConditions(strategy, 'Take Position Event') - evalFormulas(strategy, 'Take Position Event') - - 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 - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(triggerStage.takePosition.id) - tradingSystem.highlights.push(triggerStage.id) - - /* Updating Episode Counters */ - tradingEngine.episode.episodeCounters.positions.value++ - - /* Inicializing this counter */ - tradingEngine.current.distanceToEvent.takePosition.value = 1 - - tradingPositionModule.openPosition(situation.name) - - announcementsModule.makeAnnoucements(triggerStage.takePosition) - announcementsModule.makeAnnoucements(strategy.openStage) - - if (bot.SESSION.type === 'Backtesting Session') { - if (sessionParameters.snapshots !== undefined) { - if (sessionParameters.snapshots.config.position === true) { - snapshotsModule.positionEntry() - } - } - } - - tradingStrategyModule.updateStageStatus('Trigger Stage', 'Closed') - tradingStrategyModule.updateStageStatus('Open Stage', 'Opening') - tradingStrategyModule.updateStageStatus('Manage Stage', 'Opening') - } - } - } - } - } - } - } - - async function openStage() { - /* Abort Open Stage Check */ - if (tradingEngine.current.strategy.openStageStatus.value === 'Open' && tradingEngine.current.strategy.closeStageStatus.value === 'Open') { - /* - if the Close stage is opened while the open stage is still open that means that - we need to stop placing orders, check what happened to the orders already placed, - and cancel all unfilled limit orders. We call this the Closing status of the Open Stage. - */ - tradingStrategyModule.updateStageStatus('Open Stage', 'Closing') - } - - /* Opening Status Procedure */ - if (tradingEngine.current.strategy.openStageStatus.value === 'Opening') { - - /* This procedure is intended to run only once */ - let stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage - let executionNode = stageNode.openExecution - - /* Reset the Exchange Orders data structure to its initial value */ - tradingEngine.exchangeOrders.initialize(tradingEngine.exchangeOrders) - - evalConditions(stageNode, 'Initial Definition') - evalFormulas(stageNode, 'Initial Definition') - - getReadyToTakePosition() - - calculateTakeProfit() - calculateStopLoss() - - /* Remember the balance we had before taking the position to later calculate profit or loss */ - tradingEngine.previous.balance.baseAsset.value = tradingEngine.current.balance.baseAsset.value - tradingEngine.previous.balance.quotedAsset.value = tradingEngine.current.balance.quotedAsset.value - - /* Check Execution in opening stage node */ - evalConditions(stageNode, 'Open Execution') - evalFormulas(stageNode, 'Open Execution') - - await tradingExecutionModule.checkExecution(executionNode, true, false, tradingEngine.current.position.size, tradingEngine.current.position.openStageOrdersSize, tradingEngine.current.position.openStageFilledSize) - - /* From here on, the state is officially Open */ - tradingStrategyModule.updateStageStatus('Open Stage', 'Open') - return - } - - /* Open Status Procedure */ - if (tradingEngine.current.strategy.openStageStatus.value === 'Open') { - /* - While the Open Stage is Open, we do our regular stuff: place orders and check - what happened to the orders already placed. - */ - let stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage - let executionNode = stageNode.openExecution - - /* Every Loop Cycle Activity */ - evalConditions(stageNode, 'Open Execution') - evalFormulas(stageNode, 'Open Execution') - - await tradingExecutionModule.checkExecution(executionNode, false, false, tradingEngine.current.position.size, tradingEngine.current.position.openStageOrdersSize, tradingEngine.current.position.openStageFilledSize) - - /* - The Open is finished when the fillSize reaches the Position Size. - This can happens at any time when we update the filledSize value when we see - at the exchange that orders were filled. - */ - if (tradingEngine.current.position.openStageFilledSize.value === tradingEngine.current.position.size.value) { - tradingStrategyModule.updateStageStatus('Open Stage', 'Closed') - } - } - - /* Closing Status Procedure */ - if (tradingEngine.current.strategy.openStageStatus.value === 'Closing') { - /* - During the closing stage status, we do not place new orders, just check - if the ones placed were filled, and cancel the ones not filled. - */ - let stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].openStage - let executionNode = stageNode.openExecution - - /* Check if there are unfilled orders */ - if (tradingEngine.current.position.openStageOrdersSize.value !== tradingEngine.current.position.openStageFilledSize.value) { - /* There are unfilled orders, we will check if they were executed, and cancel the ones that were not. */ - - evalConditions(stageNode, 'Open Execution') - evalFormulas(stageNode, 'Open Execution') - - await tradingExecutionModule.checkExecution(executionNode, false, true, tradingEngine.current.position.size, tradingEngine.current.position.openStageOrdersSize, tradingEngine.current.position.openStageFilledSize) - } - - /* - The Closing is finished when the fillSize reaches the ordersSize. - This can happens either because we update the filledSize value when we see - at the exchange that orders were filled, or we reduce the ordersSize when - we cancel not yet filled orders. - */ - if (tradingEngine.current.position.openStageFilledSize.value === tradingEngine.current.position.openStageOrdersSize.value) { - tradingStrategyModule.updateStageStatus('Open Stage', 'Closed') - } - } - - function getReadyToTakePosition() { - - /* Position size and rate */ - tradingPositionModule.updateSizeAndRate(getPositionSize(), getPositionRate()) - - function getPositionSize() { - let balance - if (bot.sessionAndMarketBaseAssetsAreEqual) { - balance = tradingEngine.current.balance.baseAsset.value - } else { - balance = tradingEngine.current.balance.quotedAsset.value - } - const DEFAULT_VALUE = balance - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - - if (strategy.openStage === undefined) return DEFAULT_VALUE - if (strategy.openStage.initialDefinition === undefined) return DEFAULT_VALUE - if (strategy.openStage.initialDefinition.positionSize === undefined) return DEFAULT_VALUE - if (strategy.openStage.initialDefinition.positionSize.formula === undefined) return DEFAULT_VALUE - - let value = tradingSystem.formulas.get(strategy.openStage.initialDefinition.positionSize.formula.id) - if (value === undefined) return DEFAULT_VALUE - return value - } - - function getPositionRate() { - const DEFAULT_VALUE = tradingEngine.current.candle.close.value - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - - if (strategy.openStage === undefined) return DEFAULT_VALUE - if (strategy.openStage.initialDefinition === undefined) return DEFAULT_VALUE - if (strategy.openStage.initialDefinition.positionRate === undefined) return DEFAULT_VALUE - if (strategy.openStage.initialDefinition.positionRate.formula === undefined) return DEFAULT_VALUE - - let value = tradingSystem.formulas.get(strategy.openStage.initialDefinition.positionRate.formula.id) - if (value === undefined) return DEFAULT_VALUE - return value - } - } - } - - function manageStage() { - /* Opening Status Procedure */ - if (tradingEngine.current.strategy.manageStageStatus.value === 'Opening') { - /* We jump the management at the loop cycle where the position is being taken. */ - tradingStrategyModule.updateStageStatus('Manage Stage', 'Open') - return - } - - /* Open Status Procedure */ - if (tradingEngine.current.strategy.manageStageStatus.value === 'Open') { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let manageStage = strategy.manageStage - evalConditions(manageStage, 'Manage Stage') - evalFormulas(manageStage, 'Manage Stage') - - /* Stop Loss Management */ - checkStopPhasesEvents() - calculateStopLoss() - - /* Take Profit Management */ - checkTakeProfitPhaseEvents() - calculateTakeProfit() - - /* Checking if Stop or Take Profit were hit */ - checkStopLossOrTakeProfitWasHit() - } - - function checkStopPhasesEvents() { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let openStage = strategy.openStage - let manageStage = strategy.manageStage - let parentNode - let phaseIndex - let phase - let stopLoss - let stage - - if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Open Stage' && openStage !== undefined) { - stage = openStage - if (openStage.initialDefinition !== undefined) { - if (openStage.initialDefinition.initialStopLoss !== undefined) { - parentNode = openStage.initialDefinition - phaseIndex = tradingEngine.current.position.stopLoss.stopLossPhase.value - stopLoss = openStage.initialDefinition.initialStopLoss - phase = stopLoss.phases[phaseIndex] - } - } - } - - if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Manage Stage' && manageStage !== undefined) { - stage = manageStage - if (manageStage.managedStopLoss !== undefined) { - parentNode = manageStage - phaseIndex = tradingEngine.current.position.stopLoss.stopLossPhase.value - 1 - stopLoss = manageStage.managedStopLoss - phase = stopLoss.phases[phaseIndex] - } - } - - if (parentNode !== undefined) { - if (phase === undefined) { return } // trying to jump to a phase that does not exists. - - /* Check the next Phase Event. */ - let nextPhaseEvent = phase.nextPhaseEvent - if (nextPhaseEvent !== undefined) { - for (let k = 0; k < nextPhaseEvent.situations.length; k++) { - let situation = nextPhaseEvent.situations[k] - let passed - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(nextPhaseEvent.id) - tradingSystem.highlights.push(phase.id) - tradingSystem.highlights.push(stopLoss.id) - tradingSystem.highlights.push(parentNode.id) - tradingSystem.highlights.push(stage.id) - - tradingPositionModule.updateStopLoss(tradingEngine.current.position.stopLoss.stopLossPhase.value + 1, 'Manage Stage') - - announcementsModule.makeAnnoucements(nextPhaseEvent) - return // only one event can pass at the time - } - } - } - - /* Check the Move to Phase Events. */ - for (let n = 0; n < phase.moveToPhaseEvents.length; n++) { - let moveToPhaseEvent = phase.moveToPhaseEvents[n] - if (moveToPhaseEvent !== undefined) { - for (let k = 0; k < moveToPhaseEvent.situations.length; k++) { - let situation = moveToPhaseEvent.situations[k] - let passed - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(moveToPhaseEvent.id) - tradingSystem.highlights.push(phase.id) - tradingSystem.highlights.push(stopLoss.id) - tradingSystem.highlights.push(parentNode.id) - tradingSystem.highlights.push(stage.id) - - let moveToPhase = moveToPhaseEvent.referenceParent - if (moveToPhase !== undefined) { - for (let q = 0; q < stopLoss.phases.length; q++) { - if (stopLoss.phases[q].id === moveToPhase.id) { - tradingPositionModule.updateStopLoss(q + 1, 'Manage Stage') - } - } - } else { - tradingSystem.errors.push([moveToPhaseEvent.id, 'This Node needs to reference a Phase.']) - continue - } - - announcementsModule.makeAnnoucements(moveToPhaseEvent) - return // only one event can pass at the time - } - } - } - } - } - } - - function checkTakeProfitPhaseEvents() { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let openStage = strategy.openStage - let manageStage = strategy.manageStage - let parentNode - let phaseIndex - let phase - let takeProfit - let stage - - if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Open Stage' && openStage !== undefined) { - stage = openStage - if (openStage.initialDefinition !== undefined) { - if (openStage.initialDefinition.initialTakeProfit !== undefined) { - parentNode = openStage.initialDefinition - phaseIndex = tradingEngine.current.position.takeProfit.takeProfitPhase.value - takeProfit = openStage.initialDefinition.initialTakeProfit - phase = takeProfit.phases[phaseIndex] - } - } - } - - if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Manage Stage' && manageStage !== undefined) { - stage = manageStage - if (manageStage.managedTakeProfit !== undefined) { - parentNode = manageStage - phaseIndex = tradingEngine.current.position.takeProfit.takeProfitPhase.value - 1 - takeProfit = manageStage.managedTakeProfit - phase = takeProfit.phases[phaseIndex] - } - } - - if (parentNode !== undefined) { - if (phase === undefined) { return } // trying to jump to a phase that does not exists. - - /* Check the next Phase Event. */ - let nextPhaseEvent = phase.nextPhaseEvent - if (nextPhaseEvent !== undefined) { - for (let k = 0; k < nextPhaseEvent.situations.length; k++) { - let situation = nextPhaseEvent.situations[k] - let passed - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(nextPhaseEvent.id) - tradingSystem.highlights.push(phase.id) - tradingSystem.highlights.push(takeProfit.id) - tradingSystem.highlights.push(parentNode.id) - tradingSystem.highlights.push(stage.id) - - tradingPositionModule.updateTakeProfit(tradingEngine.current.position.takeProfit.takeProfitPhase.value + 1, 'Manage Stage') - - announcementsModule.makeAnnoucements(nextPhaseEvent) - return // only one event can pass at the time - } - } - } - - /* Check the Move to Phase Events. */ - for (let n = 0; n < phase.moveToPhaseEvents.length; n++) { - let moveToPhaseEvent = phase.moveToPhaseEvents[n] - if (moveToPhaseEvent !== undefined) { - for (let k = 0; k < moveToPhaseEvent.situations.length; k++) { - let situation = moveToPhaseEvent.situations[k] - let passed - if (situation.conditions.length > 0) { - passed = true - } - - passed = tradingSystem.checkConditions(situation, passed) - - tradingSystem.values.push([situation.id, passed]) - if (passed) { - tradingSystem.highlights.push(situation.id) - tradingSystem.highlights.push(moveToPhaseEvent.id) - tradingSystem.highlights.push(phase.id) - tradingSystem.highlights.push(takeProfit.id) - tradingSystem.highlights.push(parentNode.id) - tradingSystem.highlights.push(stage.id) - - let moveToPhase = moveToPhaseEvent.referenceParent - if (moveToPhase !== undefined) { - for (let q = 0; q < takeProfit.phases.length; q++) { - if (takeProfit.phases[q].id === moveToPhase.id) { - tradingPositionModule.updateTakeProfit(q + 1, 'Manage Stage') - } - } - } else { - tradingSystem.errors.push([moveToPhaseEvent.id, 'This Node needs to reference a Phase.']) - continue - } - - announcementsModule.makeAnnoucements(moveToPhaseEvent) - return // only one event can pass at the time - } - } - } - } - } - } - - function checkStopLossOrTakeProfitWasHit() { - { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - /* - 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.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.max.value >= tradingEngine.current.position.stopLoss.value) || - (!bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.min.value <= tradingEngine.current.position.stopLoss.value) - ) { - logger.write(MODULE_NAME, '[INFO] checkStopLossOrTakeProfitWasHit -> Stop Loss was hit.') - - tradingPositionModule.preventStopLossDistortion() - tradingPositionModule.applySlippageToStopLoss() - tradingPositionModule.closingPosition('Stop Loss') - tradingStrategyModule.updateStageStatus('Close Stage', 'Opening') - tradingStrategyModule.updateStageStatus('Manage Stage', 'Closed') - announcementsModule.makeAnnoucements(strategy.closeStage) - return - } - - /* Take Profit condition: Here we verify if the Take Profit was hit or not. */ - if ( - (bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.min.value <= tradingEngine.current.position.takeProfit.value) || - (!bot.sessionAndMarketBaseAssetsAreEqual && tradingEngine.current.candle.max.value >= tradingEngine.current.position.takeProfit.value) - ) { - logger.write(MODULE_NAME, '[INFO] checkStopLossOrTakeProfitWasHit -> Take Profit was hit.') - - tradingPositionModule.preventTakeProfitDistortion() - tradingPositionModule.applySlippageToTakeProfit() - tradingPositionModule.closingPosition('Take Profit') - tradingStrategyModule.updateStageStatus('Close Stage', 'Opening') - tradingStrategyModule.updateStageStatus('Manage Stage', 'Closed') - announcementsModule.makeAnnoucements(strategy.closeStage) - return - } - return - } - } - } - - async function closeStage() { - /* Opening Status Procedure */ - if (tradingEngine.current.strategy.closeStageStatus.value === 'Opening') { - /* - This will happen only once, as soon as the Take Profit or Stop was hit. - We wil not be placing orders at this time because we do not know - the total filled size of the Open Stage. By skiping execution now - we allow the open stage to get a final value for filledSize that we can use. - */ - tradingStrategyModule.updateStageStatus('Close Stage', 'Open') - return - } - - /* Open Status Procedure */ - if (tradingEngine.current.strategy.closeStageStatus.value === 'Open') { - /* - This will happen as long as the Close Stage is Open. - */ - let stageNode = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value].closeStage - let executionNode = stageNode.closeExecution - - evalConditions(stageNode, 'Close Execution') - evalFormulas(stageNode, 'Close Execution') - - await tradingExecutionModule.checkExecution(executionNode, false, false, tradingEngine.current.position.openStageFilledSize, tradingEngine.current.position.closeStageOrdersSize, tradingEngine.current.position.closeStageFilledSize) - - /* Close the Stage Validation */ - if (tradingEngine.current.position.closeStageFilledSize.value === tradingEngine.current.position.openStageFilledSize.value) { - /* - The Close Stage is closed when the fillSize reaches the filledSize of the Open Stage. - */ - tradingStrategyModule.updateStageStatus('Close Stage', 'Closed') - } - } - - /* Exit Position Validation */ - if ( - tradingEngine.current.strategy.triggerStageStatus.value === 'Closed' && - tradingEngine.current.strategy.openStageStatus.value === 'Closed' && - tradingEngine.current.strategy.manageStageStatus.value === 'Closed' && - tradingEngine.current.strategy.closeStageStatus.value === 'Closed' - ) { - - /* Exiting Everything now. */ - exitPositionAndStrategy() - } - - function exitPositionAndStrategy() { - - /* Needed for Episode Statistics */ - tradingPositionModule.updateStatistics() - - /* Recalculating Episode Counters */ - if (tradingEngine.current.position.positionStatistics.profitLoss.value > 0) { - tradingEngine.episode.episodeCounters.hits.value++ - } else { - tradingEngine.episode.episodeCounters.fails.value++ - } - - /* Recalculating Episode Statistics */ - if (bot.sessionAndMarketBaseAssetsAreEqual) { - tradingEngine.episode.episodeStatistics.profitLoss.value = tradingEngine.current.balance.baseAsset.value - sessionParameters.sessionBaseAsset.config.initialBalance - tradingEngine.episode.episodeStatistics.ROI.value = (sessionParameters.sessionBaseAsset.config.initialBalance + tradingEngine.episode.episodeStatistics.profitLoss.value) / sessionParameters.sessionBaseAsset.config.initialBalance - 1 - tradingEngine.episode.episodeStatistics.hitRatio.value = tradingEngine.episode.episodeCounters.hits.value / tradingEngine.episode.episodeCounters.positions.value - tradingEngine.episode.episodeStatistics.anualizedRateOfReturn.value = tradingEngine.episode.episodeStatistics.ROI.value / tradingEngine.episode.episodeStatistics.days.value * 365 - } else { - tradingEngine.episode.episodeStatistics.profitLoss.value = tradingEngine.current.balance.quotedAsset.value - sessionParameters.sessionQuotedAsset.config.initialBalance - tradingEngine.episode.episodeStatistics.ROI.value = (sessionParameters.sessionQuotedAsset.config.initialBalance + tradingEngine.episode.episodeStatistics.profitLoss.value) / sessionParameters.sessionQuotedAsset.config.initialBalance - 1 - tradingEngine.episode.episodeStatistics.hitRatio.value = tradingEngine.episode.episodeCounters.hits.value / tradingEngine.episode.episodeCounters.positions.value - tradingEngine.episode.episodeStatistics.anualizedRateOfReturn.value = tradingEngine.episode.episodeStatistics.ROI.value / tradingEngine.episode.episodeStatistics.days.value * 365 - } - - /* Taking Position Snapshot */ - if (bot.SESSION.type === 'Backtesting Session') { - if (sessionParameters.snapshots !== undefined) { - if (sessionParameters.snapshots.config.position === true) { - snapshotsModule.positionExit() - } - } - } - - /* Taking Strategy Snapshot */ - if (bot.SESSION.type === 'Backtesting Session') { - if (sessionParameters.snapshots !== undefined) { - if (sessionParameters.snapshots.config.strategy === true) { - snapshotsModule.strategyExit() - } - } - } - - /* Close the Position */ - tradingPositionModule.closePosition() - - /* Close the Strategy */ - tradingStrategyModule.closeStrategy('Position Closed') - - /* Distance to Events Updates */ - tradingEngine.current.distanceToEvent.closePosition.value = 1 - tradingEngine.current.distanceToEvent.triggerOff.value = 1 - } - } - - function calculateStopLoss() { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let openStage = strategy.openStage - let manageStage = strategy.manageStage - let phase - - if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Open Stage' && openStage !== undefined) { - if (openStage.initialDefinition !== undefined) { - if (openStage.initialDefinition.initialStopLoss !== undefined) { - phase = openStage.initialDefinition.initialStopLoss.phases[tradingEngine.current.position.stopLoss.stopLossPhase.value] - } - } - } - - if (tradingEngine.current.position.stopLoss.stopLossStage.value === 'Manage Stage' && manageStage !== undefined) { - if (manageStage.managedStopLoss !== undefined) { - phase = manageStage.managedStopLoss.phases[tradingEngine.current.position.stopLoss.stopLossPhase.value - 1] - } - } - - if (phase !== undefined) { - if (phase.formula !== undefined) { - let previousValue = tradingEngine.current.position.stopLoss.value - tradingPositionModule.applyStopLossFormula(tradingSystem.formulas, phase.formula.id) - - if (tradingEngine.current.position.stopLoss.value !== previousValue) { - announcementsModule.makeAnnoucements(phase) - } - } - } - } - - function calculateTakeProfit() { - let strategy = tradingSystem.tradingStrategies[tradingEngine.current.strategy.index.value] - let openStage = strategy.openStage - let manageStage = strategy.manageStage - let phase - - if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Open Stage' && openStage !== undefined) { - if (openStage.initialDefinition !== undefined) { - if (openStage.initialDefinition.initialTakeProfit !== undefined) { - phase = openStage.initialDefinition.initialTakeProfit.phases[tradingEngine.current.position.takeProfit.takeProfitPhase.value] - } - } - } - - if (tradingEngine.current.position.takeProfit.takeProfitStage.value === 'Manage Stage' && manageStage !== undefined) { - if (manageStage.managedTakeProfit !== undefined) { - phase = manageStage.managedTakeProfit.phases[tradingEngine.current.position.takeProfit.takeProfitPhase.value - 1] - } - } - - if (phase !== undefined) { - if (phase.formula !== undefined) { - let previousValue = tradingEngine.current.position.takeProfit.value - tradingPositionModule.applyTakeProfitFormula(tradingSystem.formulas, phase.formula.id) - - if (tradingEngine.current.position.takeProfit.value !== previousValue) { - announcementsModule.makeAnnoucements(phase) - } - } - } - } - - function isItInside(elementWithTimestamp, elementWithBeginEnd) { - /* - The function is to allow in Conditions and Formulas to easily know when an element with timestamp (like the ones of single files) are inside the time range - of an element with a time range, like a candle. - */ - if (elementWithTimestamp.timestamp >= elementWithBeginEnd.begin && elementWithTimestamp.timestamp <= elementWithBeginEnd.end) { - return true - } else { - return false - } - } } diff --git a/MultiPeriodDaily.js b/MultiPeriodDaily.js index 2d5e9741d..cdc4a892b 100644 --- a/MultiPeriodDaily.js +++ b/MultiPeriodDaily.js @@ -276,9 +276,19 @@ } function timeFramesLoopBody() { + const timeFrame = global.dailyFilePeriods[n][0] const timeFrameLabel = global.dailyFilePeriods[n][1] + /* Check Time Frames Filter */ + if (bot.dailyTimeFrames !== undefined) { + if (bot.dailyTimeFrames.includes(timeFrameLabel) === false) { + /* We are not going to process this Time Frame */ + timeFramesControlLoop() + return + } + } + if (processConfig.framework.validPeriods !== undefined) { let validPeriod = false; for (let i = 0; i < processConfig.framework.validPeriods.length; i++) { @@ -417,8 +427,6 @@ } function generateOutput() { - const timeFrame = global.dailyFilePeriods[n][0] - const timeFrameLabel = global.dailyFilePeriods[n][1] indicatorOutputModule.start( dataFiles, @@ -461,17 +469,18 @@ } function writeDataRanges(callBack) { + let outputDatasets = global.NODE_BRANCH_TO_ARRAY (bot.processNode.referenceParent.processOutput, 'Output Dataset') let outputDatasetIndex = -1; controlLoop() function productLoopBody() { - let productCodeName = bot.processNode.referenceParent.processOutput.outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName; + let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName; writeDataRange(contextVariables.dateBeginOfMarket, bot.multiPeriodDailyProcessDatetime, productCodeName, controlLoop) } function controlLoop() { outputDatasetIndex++ - if (outputDatasetIndex < bot.processNode.referenceParent.processOutput.outputDatasets.length) { + if (outputDatasetIndex < outputDatasets.length) { productLoopBody() } else { callBack(global.DEFAULT_OK_RESPONSE) @@ -482,7 +491,7 @@ function writeDataRange(pBegin, pEnd, productCodeName, callBack) { let dataRange = { begin: pBegin.valueOf(), - end: pEnd.valueOf() + end: pEnd.valueOf() + ONE_DAY_IN_MILISECONDS }; let fileContent = JSON.stringify(dataRange) let fileName = '/Data.Range.json'; @@ -502,6 +511,38 @@ } global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Data Range Updated', event) + writeTimeFramesFile(productCodeName, callBack) + } + } + + function writeTimeFramesFile(productCodeName, callBack) { + + let timeFramesArray = [] + for (let n = 0; n < global.dailyFilePeriods.length; n++) { + let timeFrameLabel = global.dailyFilePeriods[n][1] + + /* Check Time Frames Filter */ + if (bot.dailyTimeFrames !== undefined) { + if (bot.dailyTimeFrames.includes(timeFrameLabel) === true) { + timeFramesArray.push(timeFrameLabel) + } + } else { + timeFramesArray.push(timeFrameLabel) + } + } + + let fileContent = JSON.stringify(timeFramesArray) + let fileName = '/Time.Frames.json'; + let filePath = bot.filePathRoot + "/Output/" + productCodeName + "/" + bot.process + fileName; + + fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated) + + function onFileCreated(err) { + if (err.result !== global.DEFAULT_OK_RESPONSE.result) { + logger.write(MODULE_NAME, "[ERROR] start -> writeTimeFramesFiles -> onFileCreated -> err = " + err.stack) + callBack(err) + return + } callBack(global.DEFAULT_OK_RESPONSE) } } @@ -514,6 +555,9 @@ thisReport.file.lastFile = lastFileDate; thisReport.file.interExecutionMemoryArray = interExecutionMemoryArray; thisReport.file.beginingOfMarket = beginingOfMarket.toUTCString() + if (bot.dailyTimeFrames !== undefined) { + thisReport.file.timeFrames = bot.dailyTimeFrames + } thisReport.save(callBack) bot.hasTheBotJustStarted = false; diff --git a/MultiPeriodMarket.js b/MultiPeriodMarket.js index 3e29f55f7..76ba55b28 100644 --- a/MultiPeriodMarket.js +++ b/MultiPeriodMarket.js @@ -7,6 +7,7 @@ }; let utilities = UTILITIES.newCloudUtilities(logger) + let fileStorage = FILE_STORAGE.newFileStorage(logger) let statusDependencies; let dataDependenciesModule; @@ -31,6 +32,7 @@ } function finalize() { + fileStorage = undefined dataFiles = undefined statusDependencies = undefined dataDependenciesModule = undefined @@ -130,11 +132,66 @@ if (n < global.marketFilesPeriods.length) { timeFramesLoopBody() } else { - writeStatusReport(callBackFunction) + writeTimeFramesFiles(onTimeFrameFilesWritten) + function onTimeFrameFilesWritten() { + writeStatusReport(callBackFunction) + } } } } + function writeTimeFramesFiles(callBack) { + let outputDatasets = global.NODE_BRANCH_TO_ARRAY (bot.processNode.referenceParent.processOutput, 'Output Dataset') + let outputDatasetIndex = -1; + controlLoop() + + function productLoopBody() { + let productCodeName = outputDatasets[outputDatasetIndex].referenceParent.parentNode.config.codeName; + writeTimeFramesFile(productCodeName, controlLoop) + } + + function controlLoop() { + outputDatasetIndex++ + if (outputDatasetIndex < outputDatasets.length) { + productLoopBody() + } else { + callBack() + } + } + } + + function writeTimeFramesFile(productCodeName, callBack) { + + let timeFramesArray = [] + for (let n = 0; n < global.marketFilesPeriods.length; n++) { + let timeFrameLabel = global.marketFilesPeriods[n][1] + + /* Check Time Frames Filter */ + if (bot.marketTimeFrames !== undefined) { + if (bot.marketTimeFrames.includes(timeFrameLabel) === true) { + timeFramesArray.push(timeFrameLabel) + } + } else { + timeFramesArray.push(timeFrameLabel) + } + } + + let fileContent = JSON.stringify(timeFramesArray) + let fileName = '/Time.Frames.json'; + let filePath = bot.filePathRoot + "/Output/" + productCodeName + "/" + bot.process + fileName; + + fileStorage.createTextFile(filePath, fileContent + '\n', onFileCreated) + + function onFileCreated(err) { + if (err.result !== global.DEFAULT_OK_RESPONSE.result) { + logger.write(MODULE_NAME, "[ERROR] start -> writeTimeFramesFile -> onFileCreated -> err = " + err.stack) + callBack(err) + return + } + callBack() + } + } + function writeStatusReport(callBack) { let reportKey = bot.dataMine + "-" + bot.codeName + "-" + "Multi-Period-Market" let thisReport = statusDependencies.statusReports.get(reportKey) diff --git a/ProcessExecutionEvents.js b/ProcessExecutionEvents.js index b304be111..9b94c0365 100644 --- a/ProcessExecutionEvents.js +++ b/ProcessExecutionEvents.js @@ -1,12 +1,9 @@ exports.newProcessExecutionEvents = function newProcessExecutionEvents(BOT, logger) { - /* - - Here we manage the process execution events. This allow us to emit events when the process starts and finishes. Also to wait for a dependent event + Here we manage the process execution events. This allow us to emit events + when the process starts and finishes. Also to wait for a dependent event of another process. - */ - const MODULE_NAME = "Process Execution Events"; logger.fileName = MODULE_NAME @@ -28,12 +25,8 @@ return thisObject; - function initialize(callBackFunction) { - + function initialize(processConfig, callBackFunction) { try { - - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } - let name = 'Not Depends on any Process' logger.fileName = MODULE_NAME + "." + name; @@ -46,63 +39,108 @@ let market = bot.market.baseAsset + '/' + bot.market.quotedAsset currentProcessKey = bot.processNode.referenceParent.name + "-" + bot.processNode.referenceParent.type + "-" + bot.processNode.referenceParent.id + "-" + bot.exchange + "-" + market + /* + A process can depend for its own execution on another process to finish, and + this can be achieved in two different ways. The first one is by referencing + the Execution Started Event node from a Process Definition node to a Execution + Ended Event node of another Process Definition. The second way is when there is + a Execution Started Event node at a Process Instance. This allows the user to + override the definition created by the Data Mine for an specific task. + */ + + /* + Here we check if we can find this relationship at the Data Mine level. + */ if (bot.processNode.referenceParent.executionStartedEvent !== undefined) { if (bot.processNode.referenceParent.executionStartedEvent.referenceParent !== undefined) { if (bot.processNode.referenceParent.executionStartedEvent.referenceParent.parentNode !== undefined) { processThisDependsOn = bot.processNode.referenceParent.executionStartedEvent.referenceParent.parentNode name = processThisDependsOn.name - if (processThisDependsOn.config.monthlyInstances === true) { - logger.fileName = MODULE_NAME + "." + name; - } - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> " + currentProcessKey + " depends on " + name); } + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> " + currentProcessKey + " based on the Data Mine depends on " + name); } + } + } + } + + /* + Here we check if at the task level if there is an overriding configuration. + */ + if (bot.processNode.executionStartedEvent !== undefined) { + if (bot.processNode.executionStartedEvent.referenceParent !== undefined) { + if (bot.processNode.executionStartedEvent.referenceParent.parentNode !== undefined) { + processThisDependsOn = bot.processNode.executionStartedEvent.referenceParent.parentNode + + name = processThisDependsOn.name + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> " + currentProcessKey + " based on the User's Task depends on " + name); } } } } if (processThisDependsOn !== undefined) { + /* We need to remember that this process waits for another process in order to start. */ + processConfig.waitsForExecutionFinishedEvent = true + /* We need to find at which network node is running the process that we need to hear from when it finished. */ let network = global.TASK_NETWORK + /* + Notice that the first task that matches the dependency is going to be considered. + This means that if the same task, with the same process, pointing to the same + process definition in the same data mine, with the same exchange and market + configured, is at two different network nodes at the same time, the current task + is going to depend on the first one found, not necesarily the one running at the + same network node. If you need to control witch task specifically to depend on + and need to exactly the same task to exist, you should split the network in two + different networks, each one with unique tasks so as to avoid confussion. + */ for (let i = 0; i < network.networkNodes.length; i++) { let networkNode = network.networkNodes[i] if (networkNode.dataMining !== undefined) { - for (let j = 0; j < networkNode.dataMining.exchangeTasks.length; j++) { - let exchangeTasks = networkNode.dataMining.exchangeTasks[j] - for (let k = 0; k < exchangeTasks.taskManagers.length; k++) { - let taskManager = exchangeTasks.taskManagers[k] - for (let m = 0; m < taskManager.tasks.length; m++) { - let task = taskManager.tasks[m] - if (task.bot !== undefined) { - for (let n = 0; n < task.bot.processes.length; n++) { - let process = task.bot.processes[n] - if (process.marketReference !== undefined) { - if (process.marketReference.referenceParent !== undefined) { - let market = process.marketReference.referenceParent - let currentProcessMarket = bot.processNode.marketReference.referenceParent + for (let j = 0; j < networkNode.dataMining.exchangeDataTasks.length; j++) { + let exchangeTasks = networkNode.dataMining.exchangeDataTasks[j] + for (let p = 0; p < exchangeTasks.marketDataTasks.length; p++) { + let marketTasks = exchangeTasks.marketDataTasks[p] + if (marketTasks.referenceParent === undefined) { continue } + if (marketTasks.referenceParent.id !== global.MARKET_NODE.id) { continue } + for (let q = 0; q < marketTasks.dataMineTasks.length; q++) { + let mineTasks = marketTasks.dataMineTasks[q] + for (let k = 0; k < mineTasks.taskManagers.length; k++) { + let taskManager = mineTasks.taskManagers[k] + for (let m = 0; m < taskManager.tasks.length; m++) { + let task = taskManager.tasks[m] + if (task.bot !== undefined) { + for (let n = 0; n < task.bot.processes.length; n++) { + let process = task.bot.processes[n] + if (process.referenceParent !== undefined) { + let processDefinition = process.referenceParent + if (processThisDependsOn.id === processDefinition.id) { + /* + We found where the task that runs the process definition + we are waiting for is located on the network. + */ + thisObject.networkNode = networkNode + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { + logger.write(MODULE_NAME, + "[INFO] initialize -> Connecting to Websockets Server " + networkNode.name + + " -> host = " + networkNode.config.host + + ' -> port = ' + networkNode.config.webSocketsPort + '.'); + } - if (currentProcessMarket.id === market.id) { - if (process.referenceParent !== undefined) { - let processDefinition = process.referenceParent - if (processThisDependsOn.id === processDefinition.id) { + eventServerClient = EVENT_SERVER_CLIENT.newEventsServerClient(networkNode.config.host, networkNode.config.webSocketsPort) + eventServerClient.initialize(onConnected) - /* We found where the task that runs the process definition we are waiting for is located on the network. */ - - thisObject.networkNode = networkNode - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Connecting to Websockets Server " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webSocketsPort + '.'); } - - eventServerClient = EVENT_SERVER_CLIENT.newEventsServerClient(networkNode.config.host, networkNode.config.webSocketsPort) - eventServerClient.initialize(onConnected) - - function onConnected() { - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Connected to Websockets Server " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webSocketsPort + '.'); } - - callBackFunction(global.DEFAULT_OK_RESPONSE); - return + function onConnected() { + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { + logger.write(MODULE_NAME, "[INFO] initialize -> Connected to Websockets Server " + networkNode.name + + " -> host = " + networkNode.config.host + + ' -> port = ' + networkNode.config.webSocketsPort + '.'); } + + callBackFunction(global.DEFAULT_OK_RESPONSE); return } + return } } } diff --git a/ProcessOutput.js b/ProcessOutput.js index f9cb330d1..9f6f0ee71 100644 --- a/ProcessOutput.js +++ b/ProcessOutput.js @@ -9,87 +9,90 @@ let bot = BOT; let thisObject = { - raiseEvents, raiseEvents + raiseEvents: raiseEvents, + asyncRaiseEvents: asyncRaiseEvents }; return thisObject; function raiseEvents(lastFile, timeFrames, callBackFunction) { - try { - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] raiseEvents -> Entering function."); } - /* We will reaise the events for the datasets impacted by the process that just finished. */ if (bot.processNode.referenceParent.processOutput !== undefined) { - if (bot.processNode.referenceParent.processOutput.outputDatasets !== undefined) { - let outputDatasets = bot.processNode.referenceParent.processOutput.outputDatasets + let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset') - for (let j = 0; j < outputDatasets.length; j++) { - let outputDataset = outputDatasets[j] + for (let j = 0; j < outputDatasets.length; j++) { + let outputDataset = outputDatasets[j] - /* Some validations to verify that everything is in place. */ - if (outputDataset.referenceParent !== undefined) { - if (outputDataset.referenceParent.config.codeName === undefined) { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Dataset Definition not defined. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent)); + let botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Indicator Bot') + if (botNode === undefined) { + botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Trading Bot') + } + if (botNode === undefined) { + botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Sensor Bot') + } + + /* Some validations to verify that everything is in place. */ + if (outputDataset.referenceParent !== undefined) { + if (outputDataset.referenceParent.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Dataset Definition not defined. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent)); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return + } + if (outputDataset.referenceParent.parentNode !== undefined) { + if (outputDataset.referenceParent.parentNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Product not defined. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } - if (outputDataset.referenceParent.parentNode !== undefined) { - if (outputDataset.referenceParent.parentNode.config.codeName === undefined) { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Product not defined. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode)); + if (botNode !== undefined) { + if (botNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Bot not defined. Bot = " + JSON.stringify(botNode)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } - if (outputDataset.referenceParent.parentNode.parentNode !== undefined) { - if (outputDataset.referenceParent.parentNode.parentNode.config.codeName === undefined) { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Bot not defined. Bot = " + JSON.stringify(outputDataset.referenceParent.parentNode.parentNode)); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return - } - if (outputDataset.referenceParent.parentNode.parentNode.parentNode !== undefined) { - if (outputDataset.referenceParent.parentNode.parentNode.parentNode.config.codeName === undefined) { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Data Mine not defined. Data Mine = " + JSON.stringify(outputDataset.referenceParent.parentNode.parentNode.parentNode)); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return - } - } else { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Bot not attached to a Data Mine. Bot = " + JSON.stringify(outputDataset.referenceParent.parentNode.parentNode)); + if (botNode.parentNode !== undefined) { + if (botNode.parentNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Data Mine not defined. Data Mine = " + JSON.stringify(botNode.parentNode)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } } else { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Product not attached to a Bot. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode)); + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Bot not attached to a Data Mine or Trading Mine. Bot = " + JSON.stringify(botNode)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } } else { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Dataset Definition not attached to a Product. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent)); + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Product not attached to a Bot. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } } else { - logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Output Dataset with no reference to a Dataset Definition. Output Dataset = " + JSON.stringify(outputDataset)); + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Dataset Definition not attached to a Product. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent)); callBackFunction(global.DEFAULT_FAIL_RESPONSE); return } - - /* All good, lets emit the event. */ - - let dataMine = outputDataset.referenceParent.parentNode.parentNode.parentNode.config.codeName - let botName = outputDataset.referenceParent.parentNode.parentNode.config.codeName - let product = outputDataset.referenceParent.parentNode.config.codeName - let dataset = outputDataset.referenceParent.config.codeName - - key = dataMine + "-" + botName + "-" + product + "-" + dataset + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset - let event = { - lastFile: lastFile, - timeFrames: timeFrames - } - global.EVENT_SERVER_CLIENT.createEventHandler(key, 'Dataset Updated') - global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Dataset Updated', event) - + } else { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Output Dataset with no reference to a Dataset Definition. Output Dataset = " + JSON.stringify(outputDataset)); + callBackFunction(global.DEFAULT_FAIL_RESPONSE); + return } + + /* All good, lets emit the event. */ + + let dataMine = botNode.parentNode.config.codeName + let botName = botNode.config.codeName + let product = outputDataset.referenceParent.parentNode.config.codeName + let dataset = outputDataset.referenceParent.config.codeName + + key = dataMine + "-" + botName + "-" + product + "-" + dataset + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset + let event = { + lastFile: lastFile, + timeFrames: timeFrames + } + global.EVENT_SERVER_CLIENT.createEventHandler(key, 'Dataset Updated') + global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Dataset Updated', event) } } @@ -100,4 +103,76 @@ callBackFunction(global.DEFAULT_FAIL_RESPONSE); } } + + function asyncRaiseEvents(lastFile, timeFrames) { + + if (bot.processNode.referenceParent.processOutput !== undefined) { + let outputDatasets = global.NODE_BRANCH_TO_ARRAY(bot.processNode.referenceParent.processOutput, 'Output Dataset') + + for (let j = 0; j < outputDatasets.length; j++) { + let outputDataset = outputDatasets[j] + + let botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Indicator Bot') + if (botNode === undefined) { + botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Trading Bot') + } + if (botNode === undefined) { + botNode = global.FIND_NODE_IN_NODE_MESH(outputDataset, 'Sensor Bot') + } + + /* Some validations to verify that everything is in place. */ + if (outputDataset.referenceParent !== undefined) { + if (outputDataset.referenceParent.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Dataset Definition not defined. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + if (outputDataset.referenceParent.parentNode !== undefined) { + if (outputDataset.referenceParent.parentNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Product not defined. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + if (botNode !== undefined) { + if (botNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Bot not defined. Bot = " + JSON.stringify(botNode)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + if (botNode.parentNode !== undefined) { + if (botNode.parentNode.config.codeName === undefined) { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> codeName at Data Mine not defined. Data Mine = " + JSON.stringify(botNode.parentNode)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + } else { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Bot not attached to a Data Mine or Trading Mine. Bot = " + JSON.stringify(botNode)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + } else { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Product not attached to a Bot. Product = " + JSON.stringify(outputDataset.referenceParent.parentNode)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + } else { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Dataset Definition not attached to a Product. Dataset Definition = " + JSON.stringify(outputDataset.referenceParent)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + } else { + logger.write(MODULE_NAME, "[ERROR] raiseEvents -> Output Dataset with no reference to a Dataset Definition. Output Dataset = " + JSON.stringify(outputDataset)); + throw (global.DEFAULT_FAIL_RESPONSE); + } + + /* All good, lets emit the event. */ + + let dataMine = botNode.parentNode.config.codeName + let botName = botNode.config.codeName + let product = outputDataset.referenceParent.parentNode.config.codeName + let dataset = outputDataset.referenceParent.config.codeName + + key = dataMine + "-" + botName + "-" + product + "-" + dataset + "-" + bot.exchange + "-" + bot.market.baseAsset + '/' + bot.market.quotedAsset + let event = { + lastFile: lastFile, + timeFrames: timeFrames + } + global.EVENT_SERVER_CLIENT.createEventHandler(key, 'Dataset Updated') + global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Dataset Updated', event) + } + } + } }; diff --git a/Root.js b/Root.js index 06b1fe77b..10ac254d5 100644 --- a/Root.js +++ b/Root.js @@ -56,50 +56,29 @@ /* Global control of logging. */ global.LOG_CONTROL = { - "Assistant": { - logInfo: true, - logWarnings: false, - logErrors: true, - logContent: false, - intensiveLogging: false - }, - "Exchange API": { - logInfo: true, - logWarnings: false, - logErrors: true, - logContent: false, - intensiveLogging: false - }, "Status Report": { - logInfo: true, + logInfo: false, logWarnings: false, logErrors: true, logContent: false, intensiveLogging: false }, "Dataset": { - logInfo: true, - logWarnings: false, - logErrors: true, - logContent: false, - intensiveLogging: false - }, - "Context": { - logInfo: true, + logInfo: false, logWarnings: false, logErrors: true, logContent: false, intensiveLogging: false }, "Process Execution Events": { - logInfo: true, + logInfo: false, logWarnings: false, logErrors: true, logContent: false, intensiveLogging: false }, "Process Output": { - logInfo: true, + logInfo: false, logWarnings: false, logErrors: true, logContent: false, @@ -161,7 +140,7 @@ const FILE_STORAGE = require('./FileStorage.js'); let fileStorage = FILE_STORAGE.newFileStorage(); - let filePath = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName + '/bots/' + global.TASK_NODE.bot.config.repo + '/this.bot.config.json'; + let filePath = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName + '/bots/' + processInstance.referenceParent.parentNode.config.repo + '/this.bot.config.json'; fileStorage.getTextFile(filePath, onFileReceived); @@ -204,11 +183,11 @@ /* Simplifying the access to basic info */ botConfig.dataMine = global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName - botConfig.exchange = global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.parentNode.parentNode.name - botConfig.exchangeNode = global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.parentNode.parentNode + botConfig.exchange = global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.parentNode.parentNode.name + botConfig.exchangeNode = global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.parentNode.parentNode botConfig.market = { - baseAsset: global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.baseAsset.referenceParent.config.codeName, - quotedAsset: global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.quotedAsset.referenceParent.config.codeName + baseAsset: global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.baseAsset.referenceParent.config.codeName, + quotedAsset: global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent.quotedAsset.referenceParent.config.codeName } botConfig.uiStartDate = global.TASK_NODE.bot.config.startDate botConfig.config = global.TASK_NODE.bot.config @@ -231,16 +210,12 @@ botConfig.marketTimeFrames = global.TASK_NODE.bot.timeFramesFilter.config.marketTimeFrames } - if (botConfig.processNode) { - if (botConfig.processNode.marketReference) { - if (botConfig.processNode.marketReference.keyReference !== undefined) { - if (botConfig.processNode.marketReference.keyReference.referenceParent !== undefined) { - let key = botConfig.processNode.marketReference.keyReference.referenceParent + if (global.TASK_NODE.keyReference !== undefined) { + if (global.TASK_NODE.keyReference.referenceParent !== undefined) { + let key = global.TASK_NODE.keyReference.referenceParent - botConfig.KEY = key.config.codeName - botConfig.SECRET = key.config.secret - } - } + botConfig.KEY = key.config.codeName + botConfig.SECRET = key.config.secret } } diff --git a/SensorBot.js b/SensorBot.js index d8f8aaa32..13e5c251d 100644 --- a/SensorBot.js +++ b/SensorBot.js @@ -34,7 +34,7 @@ return } - let filePath = bot.dataMine + "/" + "bots" + "/" + bot.repo + "/" + pProcessConfig.codeName + let filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo + "/" + pProcessConfig.codeName filePath += "/User.Bot.js" fileStorage.getTextFile(filePath, onBotDownloaded); @@ -56,7 +56,7 @@ callBackFunction(global.DEFAULT_FAIL_RESPONSE); } - filePath = bot.dataMine + "/" + "bots" + "/" + bot.repo; + filePath = bot.dataMine + "/" + "bots" + "/" + bot.processNode.referenceParent.parentNode.config.repo; filePath += "/Commons.js" fileStorage.getTextFile(filePath, onCommonsDownloaded); @@ -85,9 +85,6 @@ try { let fixedTimeLoopIntervalHandle; - /* Errors sent to the UI */ - bot.processError = processError - /* Heartbeats sent to the UI */ bot.processHeartBeat = processHeartBeat @@ -148,7 +145,7 @@ function initializeProcessExecutionEvents() { try { processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger) - processExecutionEvents.initialize(onInizialized); + processExecutionEvents.initialize(processConfig, onInizialized); function onInizialized(err) { try { @@ -571,26 +568,9 @@ global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event) } - function processError(node, errorMessage) { - let event - if (node !== undefined) { - event = { - nodeName: node.name, - nodeType: node.type, - nodeId: node.id, - errorMessage: errorMessage - } - } else { - event = { - errorMessage: errorMessage - } - } - global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Error', event) - } - function processStopped() { if (global.unexpectedError !== undefined) { - processError(undefined, "An unexpected error caused the Process to stop.") + global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.") } else { global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped') } diff --git a/Session.js b/Session.js index 056d01a56..9b8c82b67 100644 --- a/Session.js +++ b/Session.js @@ -22,9 +22,9 @@ /* Set the folderName for early logging */ if (bot.processNode.session.config.folderName === undefined) { - bot.SESSION.folderName = bot.SESSION.id + bot.SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.SESSION.id } else { - bot.SESSION.folderName = bot.processNode.session.config.folderName + "-" + bot.SESSION.id + bot.SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.processNode.session.config.folderName } /* Check if there is a session */ @@ -38,11 +38,15 @@ bot.sessionKey = bot.processNode.session.name + '-' + bot.processNode.session.type + '-' + bot.processNode.session.id global.SESSION_MAP.set(bot.sessionKey, bot.sessionKey) - global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Run Session', undefined, bot.sessionKey, undefined, runSession) - global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Stop Session', undefined, bot.sessionKey, undefined, stopSession) + global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Session Status', undefined, bot.sessionKey, undefined, onSessionStatus) + global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Run Session', undefined, bot.sessionKey, undefined, onSessionRun) + global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Stop Session', undefined, bot.sessionKey, undefined, onSessionStop) + global.EVENT_SERVER_CLIENT.listenToEvent(bot.sessionKey, 'Resume Session', undefined, bot.sessionKey, undefined, onSessionResume) - /* Errors sent to the UI */ + /* Connect this here so that it is accesible from other places */ bot.sessionError = sessionError + bot.sessionWarning = sessionWarning + bot.sessionInfo = sessionInfo /* Heartbeats sent to the UI */ bot.sessionHeartBeat = sessionHeartBeat @@ -50,23 +54,40 @@ callBackFunction(global.DEFAULT_OK_RESPONSE) return - function runSession(message) { + function onSessionStatus() { + if (bot.SESSION_STATUS === 'Running') { + let event = { + status: 'Session Runnning' + } + global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Status Response', event) + } else { + let event = { + status: 'Not Session Runnning' + } + global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Status Response', event) + } + } + + function onSessionRun(message) { try { - if (bot.SESSION_STATUS === 'Idle' || bot.SESSION_STATUS === 'Running') { return } // This happens when the UI is reloaded, the session was running and tries to run it again. + /* This happens when the UI is reloaded, the session was running and tries to run it again. */ + if (bot.SESSION_STATUS === 'Idle' || bot.SESSION_STATUS === 'Running') { + parentLogger.write(MODULE_NAME, "[WARN] onSessionRun -> Event received to run the Session while it was already running. ") + return + } /* We are going to run the Definition comming at the event. */ - bot.APP_SCHEMA_ARRAY = JSON.parse(message.event.appSchema) bot.TRADING_SYSTEM = JSON.parse(message.event.tradingSystem) bot.TRADING_ENGINE = JSON.parse(message.event.tradingEngine) bot.SESSION = JSON.parse(message.event.session) bot.DEPENDENCY_FILTER = JSON.parse(message.event.dependencyFilter) - bot.RESUME = message.event.resume + bot.RESUME = false bot.FIRST_EXECUTION = true bot.SESSION.stop = stopSession // stop function - setUpAppSchema() setUpSessionFolderName() + /* We validate all parameters received and complete some that might be missing if needed. */ if (checkParemeters() === false) { return } socialBotsModule.initialize() @@ -95,44 +116,63 @@ bot.STOP_SESSION = false } else { bot.STOP_SESSION = true + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] onSessionRun -> Stopping the Session now. ') } } socialBotsModule.sendMessage(bot.SESSION.type + " '" + bot.SESSION.name + "' is starting.") } catch (err) { - parentLogger.write(MODULE_NAME, "[ERROR] initialize -> runSession -> err = " + err.stack); + parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onSessionRun -> err = " + err.stack); + } + } + + function onSessionStop() { + stopSession('Session Stopped From the User Interface.') + } + + function onSessionResume(message) { + try { + if (bot.SESSION.stop === undefined) { + parentLogger.write(MODULE_NAME, "[WARN] onSessionResume -> Event received to resume the Session that have never be ran before. ") + return + } + + /* This happens when the UI is reloaded, the session was running and tries to run it again. */ + if (bot.SESSION_STATUS === 'Idle' || bot.SESSION_STATUS === 'Running') { + parentLogger.write(MODULE_NAME, "[WARN] onSessionResume -> Event received to resume the Session while it was already running. ") + return + } + + bot.RESUME = true + bot.STOP_SESSION = false + + socialBotsModule.sendMessage(bot.SESSION.type + " '" + bot.SESSION.name + "' is resuming.") + } catch (err) { + parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onSessionResume -> err = " + err.stack); } } function stopSession(commandOrigin) { - try { - if (commandOrigin === undefined) { commandOrigin = ' from the User Interface.' } - socialBotsModule.sendMessage(bot.SESSION.type + " '" + bot.SESSION.name + "' is stopping " + commandOrigin) - socialBotsModule.finalize() - bot.STOP_SESSION = true - } catch (err) { - parentLogger.write(MODULE_NAME, "[ERROR] initialize -> stopSession -> err = " + err.stack); - } - } - function setUpAppSchema() { - /* Setup the APP_SCHEMA_MAP based on the APP_SCHEMA_ARRAY */ - bot.APP_SCHEMA_MAP = new Map() - for (let i = 0; i < bot.APP_SCHEMA_ARRAY.length; i++) { - let nodeDefinition = bot.APP_SCHEMA_ARRAY[i] - let key = nodeDefinition.type - bot.APP_SCHEMA_MAP.set(key, nodeDefinition) - } + socialBotsModule.sendMessage(bot.SESSION.type + " '" + bot.SESSION.name + "' is stopping " + commandOrigin) + socialBotsModule.finalize() + bot.STOP_SESSION = true + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] stopSession -> Stopping the Session now. ') } + sessionInfo(bot.SESSION, commandOrigin) } function setUpSessionFolderName() { - /* Set the folderName for logging, reports, context and data output */ + /* + The session object is overwritten when the session is run. For that reason we + need to setup again the folder name at the Session level. + Set the folderName for logging, reports, context and data output + */ let config if (bot.SESSION.config !== undefined) { config = bot.SESSION.config if (config.folderName === undefined) { - bot.SESSION.folderName = bot.SESSION.id + bot.SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.SESSION.id } else { - bot.SESSION.folderName = config.folderName + "-" + bot.SESSION.id + bot.SESSION.folderName = bot.processNode.session.type.replace(' ', '-').replace(' ', '-') + '-' + bot.processNode.session.config.folderName } } } @@ -157,13 +197,29 @@ type: 'Time Range', config: { initialDatetime: (new Date()).valueOf(), - finalDatetime: (new Date()).valueOf() + finalDatetime: (new Date()).valueOf() + global.ONE_YEAR_IN_MILISECONDS } } + } else { + /* Check that we received valid dates */ + if (bot.SESSION.type === 'Backtesting Session') { + if (isNaN(new Date(bot.SESSION.parameters.timeRange.config.initialDatetime)).valueOf()) { + let errorMessage = "sessionParameters.timeRange.config.initialDatetime is not a valid date." + parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage) + bot.sessionError(bot.SESSION.parameters, errorMessage) + return false + } + } + if (isNaN(new Date(bot.SESSION.parameters.timeRange.config.finalDatetime)).valueOf()) { + let errorMessage = "sessionParameters.timeRange.config.initialDatetime is not a valid date." + parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage) + bot.sessionError(bot.SESSION.parameters, errorMessage) + return false + } } /* Session Type Forced Values */ - let today = new Date((new Date()).valueOf()) + let today = (new Date()).valueOf() let aYearAgo = today - global.ONE_YEAR_IN_MILISECONDS let aYearFromNow = today + global.ONE_YEAR_IN_MILISECONDS switch (bot.SESSION.type) { @@ -186,11 +242,25 @@ } function useDefaultDatetimes(initialDefault, finalDefault) { + /* + Note that inside the system, we are going to deal with these + dates in their numeric value representation. + */ + /* Initial Datetime */ - if (bot.SESSION.parameters.timeRange.config.initialDatetime === undefined) { - bot.SESSION.parameters.timeRange.config.initialDatetime = initialDefault + if (bot.SESSION.type === 'Backtesting Session') { + if (bot.SESSION.parameters.timeRange.config.initialDatetime === undefined) { + bot.SESSION.parameters.timeRange.config.initialDatetime = initialDefault + } else { + bot.SESSION.parameters.timeRange.config.initialDatetime = (new Date(bot.SESSION.parameters.timeRange.config.initialDatetime)).valueOf() + } } else { - bot.SESSION.parameters.timeRange.config.initialDatetime = (new Date(bot.SESSION.parameters.timeRange.config.initialDatetime)).valueOf() + /* Non backtest session can start from the past only if explicitly configured that way */ + if (bot.SESSION.parameters.timeRange.config.initialDatetime === undefined || bot.SESSION.parameters.timeRange.config.allowStartingFromThePast !== true) { + bot.SESSION.parameters.timeRange.config.initialDatetime = initialDefault + } else { + bot.SESSION.parameters.timeRange.config.initialDatetime = (new Date(bot.SESSION.parameters.timeRange.config.initialDatetime)).valueOf() + } } /* Final Datetime */ @@ -215,6 +285,12 @@ return false } bot.SESSION.parameters.timeFrame.config.value = getTimeFrameFromLabel(bot.SESSION.parameters.timeFrame.config.label) + if (bot.SESSION.parameters.timeFrame.config.value === undefined) { + let errorMessage = "Config error: label value not recognized. Try 01-min or 01-hs for example." + parentLogger.write(MODULE_NAME, "[ERROR] initialize -> checkParemeters -> " + errorMessage) + bot.sessionError(bot.SESSION.parameters.timeFrame, errorMessage) + return false + } /* Session Base Asset */ if (bot.SESSION.parameters.sessionBaseAsset === undefined) { @@ -229,12 +305,6 @@ bot.sessionError(bot.SESSION.parameters.sessionBaseAsset, errorMessage) return false } - if (bot.SESSION.parameters.sessionBaseAsset.config.minimumBalance === undefined) { - bot.SESSION.parameters.sessionBaseAsset.config.minimumBalance = 0 - } - if (bot.SESSION.parameters.sessionBaseAsset.config.maximumBalance === undefined) { - bot.SESSION.parameters.sessionBaseAsset.config.maximumBalance = Number.MAX_SAFE_INTEGER - } /* Session Quoted Asset */ if (bot.SESSION.parameters.sessionQuotedAsset === undefined) { @@ -249,12 +319,6 @@ bot.sessionError(bot.SESSION.parameters.sessionQuotedAsset, errorMessage) return false } - if (bot.SESSION.parameters.sessionQuotedAsset.config.minimumBalance === undefined) { - bot.SESSION.parameters.sessionQuotedAsset.config.minimumBalance = 0 - } - if (bot.SESSION.parameters.sessionQuotedAsset.config.maximumBalance === undefined) { - bot.SESSION.parameters.sessionQuotedAsset.config.maximumBalance = Number.MAX_SAFE_INTEGER - } /* Slippage */ if (bot.SESSION.parameters.slippage === undefined) { // if the Slippage is missing we create a default one. @@ -300,7 +364,6 @@ } function startBackTesting(message) { - bot.tradingProcessDate = new Date(bot.SESSION.parameters.timeRange.config.initialDatetime.valueOf()) // Here we initialize the date of the process. return true } @@ -309,9 +372,6 @@ if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, "[ERROR] initialize -> startLiveTrading -> Key 'codeName' or 'secret' not provided. Plese check that and try again."); } return false } - - bot.tradingProcessDate = new Date(bot.SESSION.parameters.timeRange.config.initialDatetime.valueOf()) - pProcessConfig.liveWaitTime = bot.SESSION.parameters.timeFrame.config.value return true } @@ -322,8 +382,6 @@ return false } - bot.tradingProcessDate = new Date(bot.SESSION.parameters.timeRange.config.initialDatetime.valueOf()) - /* Reduce the balance */ let balancePercentage = 1 // This is the default value @@ -346,8 +404,6 @@ } function startPaperTrading(message) { - bot.tradingProcessDate = new Date(bot.SESSION.parameters.timeRange.config.initialDatetime.valueOf()) - pProcessConfig.normalWaitTime = bot.SESSION.parameters.timeFrame.config.value return true } @@ -372,15 +428,18 @@ } } - function sessionHeartBeat(processingDate) { + function sessionHeartBeat(processingDate, percentage, status) { let event = { seconds: (new Date()).getSeconds(), - processingDate: processingDate + processingDate: processingDate, + percentage: percentage, + status: status } global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Heartbeat', event) if (global.STOP_TASK_GRACEFULLY === true) { bot.STOP_SESSION = true + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionHeartBeat -> Stopping the Session now. ') } } } @@ -402,6 +461,51 @@ if (global.STOP_TASK_GRACEFULLY === true) { bot.STOP_SESSION = true + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionError -> Stopping the Session now. ') } + } + } + + function sessionWarning(node, warningMessage) { + let event + if (node !== undefined) { + event = { + nodeName: node.name, + nodeType: node.type, + nodeId: node.id, + warningMessage: warningMessage + } + } else { + event = { + warningMessage: warningMessage + } + } + global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Warning', event) + + if (global.STOP_TASK_GRACEFULLY === true) { + bot.STOP_SESSION = true + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionWarning -> Stopping the Session now. ') } + } + } + + function sessionInfo(node, infoMessage) { + let event + if (node !== undefined) { + event = { + nodeName: node.name, + nodeType: node.type, + nodeId: node.id, + infoMessage: infoMessage + } + } else { + event = { + infoMessage: infoMessage + } + } + global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Info', event) + + if (global.STOP_TASK_GRACEFULLY === true) { + bot.STOP_SESSION = true + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] sessionInfo -> Stopping the Session now. ') } } } diff --git a/StatusDependencies.js b/StatusDependencies.js index 21314c4c8..2b3615e32 100644 --- a/StatusDependencies.js +++ b/StatusDependencies.js @@ -18,11 +18,7 @@ return thisObject; function initialize(callBackFunction) { - try { - - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } - /* Basic Valdidations */ if (bot.processNode.referenceParent.processDependencies !== undefined) { if (bot.processNode.referenceParent.processDependencies.statusDependencies !== undefined) { @@ -88,9 +84,6 @@ switch (err.message) { case global.DEFAULT_OK_RESPONSE.message: { - - if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] initialize -> onLoad -> Execution finished well. -> Status Dependency = " + JSON.stringify(statusDependency.dependency)) } - addReport(); return; } diff --git a/StatusReport.js b/StatusReport.js index 4da076767..21089be6d 100644 --- a/StatusReport.js +++ b/StatusReport.js @@ -20,6 +20,7 @@ finalize: finalize, load: load, save: save, + asyncSave: asyncSave, status: undefined }; @@ -39,10 +40,7 @@ return thisObject; function initialize(pStatusDependencyNode, callBackFunction) { - try { - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Entering function."); } - statusDependencyNode = pStatusDependencyNode; logger.fileName = MODULE_NAME + "." + statusDependencyNode.type + "." + statusDependencyNode.name + "." + statusDependencyNode.id; @@ -85,7 +83,7 @@ function validationFailed(errorInNode, errorMessage) { let nodeString = JSON.stringify(errorInNode) logger.write(MODULE_NAME, "[ERROR] initialize -> " + errorMessage + ' -> nodeString = ' + nodeString) - bot.processError(errorInNode, errorMessage) + global.PROCESS_ERROR(bot.processKey, errorInNode, errorMessage) callBackFunction(global.DEFAULT_FAIL_RESPONSE); } @@ -113,50 +111,97 @@ for (let i = 0; i < network.networkNodes.length; i++) { let networkNode = network.networkNodes[i] - if (checkThisBranch(networkNode.dataMining) === true) { return } - if (checkThisBranch(networkNode.testingEnvironment) === true) { return } - if (checkThisBranch(networkNode.productionEnvironment) === true) { return } + if (checkThisDataBranch(networkNode.dataMining) === true) { return } + if (checkThisTradingBranch(networkNode.testingEnvironment) === true) { return } + if (checkThisTradingBranch(networkNode.productionEnvironment) === true) { return } - function checkThisBranch(branch) { + function checkThisDataBranch(branch) { if (branch === undefined) { return } - for (let j = 0; j < branch.exchangeTasks.length; j++) { - let exchangeTasks = branch.exchangeTasks[j] - for (let k = 0; k < exchangeTasks.taskManagers.length; k++) { - let taskManager = exchangeTasks.taskManagers[k] - for (let m = 0; m < taskManager.tasks.length; m++) { - let task = taskManager.tasks[m] - if (task.bot !== undefined) { - for (let n = 0; n < task.bot.processes.length; n++) { - let process = task.bot.processes[n] - if (process.marketReference !== undefined) { - if (process.marketReference.referenceParent !== undefined) { - let market = process.marketReference.referenceParent - let currentProcessMarket = bot.processNode.marketReference.referenceParent - if (currentProcessMarket.id === market.id) { - if (process.referenceParent !== undefined) { - let processDefinition = process.referenceParent - if (processThisDependsOn.id === processDefinition.id) { - if (process.type === 'Trading Process Instance') { - if (process.session !== undefined) { - if (bot.processNode.session.id !== process.session.id) { - continue - } - } else { - continue - } - } - - /* We found where the task that runs the process definition this status report depends on and where it is located on the network. */ - - thisObject.networkNode = networkNode - if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving status report from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); } - - fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort); - callBackFunction(global.DEFAULT_OK_RESPONSE); - return true + for (let j = 0; j < branch.exchangeDataTasks.length; j++) { + let exchangeTasks = branch.exchangeDataTasks[j] + for (let p = 0; p < exchangeTasks.marketDataTasks.length; p++) { + let marketTasks = exchangeTasks.marketDataTasks[p] + if (marketTasks.referenceParent === undefined) { continue } + if (marketTasks.referenceParent.id !== global.MARKET_NODE.id) { continue } + for (let q = 0; q < marketTasks.dataMineTasks.length; q++) { + let mineTasks = marketTasks.dataMineTasks[q] + for (let k = 0; k < mineTasks.taskManagers.length; k++) { + let taskManager = mineTasks.taskManagers[k] + for (let m = 0; m < taskManager.tasks.length; m++) { + let task = taskManager.tasks[m] + if (task.bot === undefined) { continue } + for (let n = 0; n < task.bot.processes.length; n++) { + let process = task.bot.processes[n] + if (process.referenceParent === undefined) { continue } + let processDefinition = process.referenceParent + if (processThisDependsOn.id === processDefinition.id) { + if (process.type === 'Trading Process Instance') { + if (process.session !== undefined) { + if (bot.processNode.session.id !== process.session.id) { + continue } + } else { + continue } } + /* + We found where the task that runs the process definition this status report depends on + and where it is located on the network. + */ + thisObject.networkNode = networkNode + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving status report from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); } + + fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort); + callBackFunction(global.DEFAULT_OK_RESPONSE); + return true + } + } + } + } + } + } + } + } + + function checkThisTradingBranch(branch) { + if (branch === undefined) { return } + for (let j = 0; j < branch.exchangeTradingTasks.length; j++) { + let exchangeTasks = branch.exchangeTradingTasks[j] + for (let p = 0; p < exchangeTasks.marketTradingTasks.length; p++) { + let marketTasks = exchangeTasks.marketTradingTasks[p] + if (marketTasks.referenceParent === undefined) { continue } + if (marketTasks.referenceParent.id !== global.MARKET_NODE.id) { continue } + for (let q = 0; q < marketTasks.tradingMineTasks.length; q++) { + let mineTasks = marketTasks.tradingMineTasks[q] + for (let k = 0; k < mineTasks.taskManagers.length; k++) { + let taskManager = mineTasks.taskManagers[k] + for (let m = 0; m < taskManager.tasks.length; m++) { + let task = taskManager.tasks[m] + if (task.bot === undefined) { continue } + for (let n = 0; n < task.bot.processes.length; n++) { + let process = task.bot.processes[n] + if (process.referenceParent === undefined) { continue } + let processDefinition = process.referenceParent + if (processThisDependsOn.id === processDefinition.id) { + if (process.type === 'Trading Process Instance') { + if (process.session !== undefined) { + if (bot.processNode.session.id !== process.session.id) { + continue + } + } else { + continue + } + } + /* + We found where the task that runs the process definition this status report depends on + and where it is located on the network. + */ + thisObject.networkNode = networkNode + if (global.LOG_CONTROL[MODULE_NAME].logInfo === true) { logger.write(MODULE_NAME, "[INFO] initialize -> Retrieving status report from " + networkNode.name + " -> host = " + networkNode.config.host + ' -> port = ' + networkNode.config.webPort + '.'); } + + fileStorage = FILE_STORAGE.newFileStorage(logger, networkNode.config.host, networkNode.config.webPort); + callBackFunction(global.DEFAULT_OK_RESPONSE); + return true } } } @@ -346,4 +391,22 @@ } } -}; + async function asyncSave() { + + let fileName = "Status.Report.json"; + let filePath = bot.filePathRoot + "/Reports/" + sessionPath + statusDependencyNode.process; + + filePath += '/' + fileName + let fileContent = JSON.stringify(thisObject.file); + + let response = await fileStorage.asyncCreateTextFile(filePath, fileContent + '\n', true) + + if (response.err.result !== global.DEFAULT_OK_RESPONSE.result) { + throw (response.err) + } + + /* All good, lets emit the event that means data has been updated. */ + let processOutput = PROCESS_OUTPUT.newProcessOutput(bot, logger) + processOutput.asyncRaiseEvents(thisObject.file.lastFile, thisObject.file.timeFrames) + } +} diff --git a/TaskServer.sln b/TaskServer.sln deleted file mode 100644 index a21837e88..000000000 --- a/TaskServer.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "Project", "Project.njsproj", "{AE0A3FE2-7021-4CC2-9922-A887CA5CDD7C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AE0A3FE2-7021-4CC2-9922-A887CA5CDD7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE0A3FE2-7021-4CC2-9922-A887CA5CDD7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE0A3FE2-7021-4CC2-9922-A887CA5CDD7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE0A3FE2-7021-4CC2-9922-A887CA5CDD7C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9128DF06-8CA5-497C-8BF7-B924571C5394} - EndGlobalSection -EndGlobal diff --git a/TradingBot.js b/TradingBot.js index 8b16f955e..9d4d48ee7 100644 --- a/TradingBot.js +++ b/TradingBot.js @@ -3,9 +3,6 @@ const MODULE_NAME = "Trading Bot"; const FULL_LOG = true; - let USER_BOT_MODULE; - let USER_BOT_COMMONS; - const TRADING_PROCESS_MODULE = require(global.ROOT_DIR + '/LowFrequencyTrading/TradingProcess.js'); const FILE_STORAGE = require('./FileStorage.js'); const SESSION = require(global.ROOT_DIR + 'Session'); @@ -33,53 +30,9 @@ processConfig = pProcessConfig; if (bot.definedByUI === true) { /* The code of the bot is defined at the UI. No need to load a file with the code. */ - session.initialize(processConfig, onSessionInitialized) - - function onSessionInitialized(err) { - callBackFunction(err); - } - return + session.initialize(processConfig, callBackFunction) } - let filePath = bot.dataMine + "/" + "bots" + "/" + bot.repo + "/" + pProcessConfig.codeName - filePath += "/User.Bot.js" - - fileStorage.getTextFile(filePath, onBotDownloaded); - - function onBotDownloaded(err, text) { - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - - parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onInizialized -> onBotDownloaded -> err = " + err.message); - parentLogger.write(MODULE_NAME, "[ERROR] initialize -> onInizialized -> onBotDownloaded -> filePath = " + filePath); - callBackFunction(global.DEFAULT_FAIL_RESPONSE); - return; - } - - USER_BOT_MODULE = {} - USER_BOT_MODULE.newUserBot = eval(text); // Use this for production - - filePath = bot.dataMine + "/" + "bots" + "/" + bot.repo; - filePath += "/Commons.js" - - fileStorage.getTextFile(filePath, onCommonsDownloaded); - - function onCommonsDownloaded(err, text) { - if (err.result !== global.DEFAULT_OK_RESPONSE.result) { - parentLogger.write(MODULE_NAME, "[WARN] initialize -> onBotDownloaded -> onCommonsDownloaded -> Commons not found: " + err.code || err.message); - callBackFunction(global.DEFAULT_OK_RESPONSE); - return; - } - - USER_BOT_COMMONS = {} - USER_BOT_COMMONS.newCommons = eval(text); // Use this for production - - session.initialize(onSessionInitialized) - - function onSessionInitialized(err) { - callBackFunction(err); - } - } - } } catch (err) { parentLogger.write(MODULE_NAME, "[ERROR] initialize -> err = " + err.stack); callBackFunction(global.DEFAULT_FAIL_RESPONSE); @@ -91,9 +44,7 @@ /* Some initial values*/ let fixedTimeLoopIntervalHandle; bot.STOP_SESSION = true; - - /* Errors sent to the UI */ - bot.processError = processError + if (FULL_LOG === true) { parentLogger.write(MODULE_NAME, '[IMPORTANT] run -> Stopping the Session now. ') } /* Heartbeats sent to the UI */ bot.processHeartBeat = processHeartBeat @@ -123,7 +74,7 @@ let nextWaitTime; /* We tell the UI that we are running. */ - processHeartBeat() + processHeartBeat(undefined, undefined, "Running...") /* We define here all the modules that the rest of the infraestructure, including the bots themselves can consume. */ const UTILITIES = require(global.ROOT_DIR + 'CloudUtilities'); @@ -143,14 +94,14 @@ /* Checking if we need to need to emit any event */ if (bot.SESSION_STATUS === 'Idle' && bot.STOP_SESSION === false) { - global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Running') bot.SESSION_STATUS = 'Running' } if (bot.SESSION_STATUS === 'Running' && bot.STOP_SESSION === true) { - global.EVENT_SERVER_CLIENT.raiseEvent(bot.sessionKey, 'Stopped') bot.SESSION_STATUS = 'Stopped' - } + } + + global.EMIT_SESSION_STATUS (bot.SESSION_STATUS, bot.sessionKey) /* Checking if we should process this loop or not.*/ if (bot.STOP_SESSION === true) { @@ -178,7 +129,7 @@ function initializeProcessExecutionEvents() { try { processExecutionEvents = PROCESS_EXECUTION_EVENTS.newProcessExecutionEvents(bot, logger) - processExecutionEvents.initialize(onInizialized); + processExecutionEvents.initialize(processConfig, onInizialized); function onInizialized(err) { try { @@ -337,7 +288,7 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> initializeDataDependencies -> onInizialized -> Execution finished well."); } switch (processConfig.framework.name) { case 'Low-Frequency-Trading-Process': { - processFramework = TRADING_PROCESS_MODULE.newTradingProcess(bot, logger, UTILITIES, USER_BOT_MODULE, USER_BOT_COMMONS); + processFramework = TRADING_PROCESS_MODULE.newTradingProcess(bot, logger, UTILITIES); intitializeProcessFramework(); break; } @@ -572,7 +523,7 @@ if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> nextWaitTime = " + nextWaitTime); } /* We show we reached the end of the loop. */ - processHeartBeat() + processHeartBeat(undefined, undefined, "Running...") /* Here we check if we must stop the loop gracefully. */ shallWeStop(onStop, onContinue); @@ -607,23 +558,27 @@ break case 'Normal': { let waitTime - switch (bot.SESSION.type) { - case 'Live Trading Session': { - waitTime = processConfig.liveTradingWaitTime + if (processConfig.waitsForExecutionFinishedEvent === true) { + waitTime = 0 + } else { + switch (bot.SESSION.type) { + case 'Live Trading Session': { + waitTime = bot.SESSION.parameters.timeFrame.config.value + break + } + case 'Fordward Tessting Session': { + waitTime = bot.SESSION.parameters.timeFrame.config.value + break + } + case 'Paper Trading Session': { + waitTime = bot.SESSION.parameters.timeFrame.config.value + break + } + case 'Backtesting Session': { + waitTime = 0 + break + } } - break - case 'Fordward Tessting Session': { - waitTime = processConfig.fordwardTestingWaitTime - } - break - case 'Paper Trading Session': { - waitTime = processConfig.paperTradingWaitTime - } - break - case 'Backtesting Session': { - waitTime = processConfig.backtestingWaitTime - } - break } if (FULL_LOG === true) { logger.write(MODULE_NAME, "[INFO] run -> loop -> loopControl -> Restarting Loop in " + (waitTime / 1000 / 60) + " minute/s."); } @@ -697,26 +652,9 @@ global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Heartbeat', event) } - function processError(node, errorMessage) { - let event - if (node !== undefined) { - event = { - nodeName: node.name, - nodeType: node.type, - nodeId: node.id, - errorMessage: errorMessage - } - } else { - event = { - errorMessage: errorMessage - } - } - global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Error', event) - } - function processStopped() { if (global.unexpectedError !== undefined) { - processError(undefined, "An unexpected error caused the Process to stop.") + global.PROCESS_ERROR(bot.processKey, undefined, "An unexpected error caused the Process to stop.") } else { global.EVENT_SERVER_CLIENT.raiseEvent(bot.processKey, 'Stopped') } diff --git a/server.js b/server.js index b9e30080f..3ff27cf3f 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,6 @@ -/* Callbacks default responses. */ + +/* Callbacks default responses. */ global.DEFAULT_OK_RESPONSE = { result: "Ok", message: "Operation Succeeded" @@ -41,7 +42,260 @@ global.UNIQUE_ID = function () { } global.PRECISE = function (floatNumber, precision) { - return parseFloat(Number.parseFloat(floatNumber).toPrecision(precision)) + return this.parseFloat(floatNumber.toFixed(precision)) +} + +global.REMOVE_TIME = function (datetime) { + const GMT_SECONDS = ':00.000 GMT+0000'; + let date = new Date(datetime) + return new Date(date.getUTCFullYear() + "-" + (date.getUTCMonth() + 1) + "-" + date.getUTCDate() + " " + "00:00" + GMT_SECONDS); +} + +global.PROCESS_ERROR = function (processKey, node, errorMessage) { + let event + if (node !== undefined) { + event = { + nodeName: node.name, + nodeType: node.type, + nodeId: node.id, + errorMessage: errorMessage + } + } else { + event = { + errorMessage: errorMessage + } + } + global.EVENT_SERVER_CLIENT.raiseEvent(processKey, 'Error', event) +} + +global.PROCESS_WARNING = function (processKey, node, warningMessage) { + let event + if (node !== undefined) { + event = { + nodeName: node.name, + nodeType: node.type, + nodeId: node.id, + warningMessage: warningMessage + } + } else { + event = { + warningMessage: warningMessage + } + } + global.EVENT_SERVER_CLIENT.raiseEvent(processKey, 'Warning', event) +} + +global.PROCESS_INFO = function (processKey, node, infoMessage) { + let event + if (node !== undefined) { + event = { + nodeName: node.name, + nodeType: node.type, + nodeId: node.id, + infoMessage: infoMessage + } + } else { + event = { + infoMessage: infoMessage + } + } + global.EVENT_SERVER_CLIENT.raiseEvent(processKey, 'Info', event) +} + +global.NODE_BRANCH_TO_ARRAY = function (node, nodeType) { + let resultArray = [] + scanNodeBranch(node, nodeType) + return resultArray + + function scanNodeBranch(startingNode) { + let nodeDefinition = global.APP_SCHEMA_MAP.get(startingNode.type) + if (nodeDefinition === undefined) { return } + + if (startingNode.type === nodeType) { + resultArray.push(startingNode) + return + } + + if (nodeDefinition.properties === undefined) { return } + for (let i = 0; i < nodeDefinition.properties.length; i++) { + let property = nodeDefinition.properties[i] + + switch (property.type) { + case 'node': { + scanNodeBranch(startingNode[property.name]) + } + break + case 'array': { + let startingNodePropertyArray = startingNode[property.name] + if (startingNodePropertyArray !== undefined) { + for (let m = 0; m < startingNodePropertyArray.length; m++) { + scanNodeBranch(startingNodePropertyArray[m]) + } + } + break + } + } + } + } +} + +global.FIND_NODE_IN_NODE_MESH = function (node, nodeType) { + /* + This function scans a node mesh for a certain node type and + returns the first instance found. + */ + let nodeFound + scanNodeMesh(node, nodeType) + return nodeFound + + function scanNodeMesh(startingNode) { + if (startingNode === undefined) { return } + if (nodeFound !== undefined) { return } + + let nodeDefinition = APP_SCHEMA_MAP.get(startingNode.type) + if (nodeDefinition === undefined) { return } + + if (startingNode.type === nodeType) { + nodeFound = startingNode + return + } + + /* We scan through this node children */ + if (nodeDefinition.properties !== undefined) { + for (let i = 0; i < nodeDefinition.properties.length; i++) { + let property = nodeDefinition.properties[i] + + switch (property.type) { + case 'node': { + scanNodeMesh(startingNode[property.name]) + } + break + case 'array': { + let startingNodePropertyArray = startingNode[property.name] + if (startingNodePropertyArray !== undefined) { + for (let m = 0; m < startingNodePropertyArray.length; m++) { + scanNodeMesh(startingNodePropertyArray[m]) + } + } + break + } + } + } + } + /* We scan parents nodes. */ + if (startingNode.parentNode !== undefined) { + scanNodeMesh(startingNode.parentNode) + } + /* We scan reference parents too. */ + if (startingNode.referenceParent !== undefined) { + scanNodeMesh(startingNode.referenceParent) + } + } +} + +global.NODE_MESH_TO_PATH_ARRAY = function (node, nodeId) { + /* + This function scans a node mesh for a certain node if and + returns an array with the path within that mesh to the + requested node. + */ + let nodeArray = [] + scanNodeMesh(node) + return nodeArray + + function scanNodeMesh(startingNode) { + if (startingNode === undefined) { return } + + let nodeDefinition = APP_SCHEMA_MAP.get(startingNode.type) + if (nodeDefinition === undefined) { return } + + if (startingNode.id === nodeId) { + nodeArray.push(startingNode) + return + } + + /* We scan through this node children */ + if (nodeDefinition.properties !== undefined) { + for (let i = 0; i < nodeDefinition.properties.length; i++) { + let property = nodeDefinition.properties[i] + + switch (property.type) { + case 'node': { + scanNodeMesh(startingNode[property.name]) + if (nodeArray.length > 0) { + nodeArray.unshift(startingNode) + return + } + } + break + case 'array': { + let startingNodePropertyArray = startingNode[property.name] + if (startingNodePropertyArray !== undefined) { + for (let m = 0; m < startingNodePropertyArray.length; m++) { + scanNodeMesh(startingNodePropertyArray[m]) + if (nodeArray.length > 0) { + nodeArray.unshift(startingNode) + return + } + } + } + break + } + } + } + } + /* We scan parents nodes. */ + if (startingNode.parentNode !== undefined) { + scanNodeMesh(startingNode.parentNode) + if (nodeArray.length > 0) { + nodeArray.unshift(startingNode) + return + } + } + /* We scan reference parents too. */ + if (startingNode.referenceParent !== undefined) { + scanNodeMesh(startingNode.referenceParent) + if (nodeArray.length > 0) { + nodeArray.unshift(startingNode) + return + } + } + } +} + +global.FIND_NODE_IN_NODE_ARRAY = function (nodeArray, nodeType) { + for (let i=0; i < nodeArray.length; i++) { + let node = nodeArray[i] + if (node.type === nodeType) { + return node + } + } +} + +global.FILTER_OUT_NODES_WITHOUT_REFERENCE_PARENT_FROM_NODE_ARRAY = function (nodeArray) { + let filteredNodeArray = [] + for (let i = 0; i < nodeArray.length; i++) { + let arrayItem = nodeArray[i] + if (arrayItem.referenceParent === undefined) { + continue + } else { + filteredNodeArray.push(arrayItem) + } + } + return filteredNodeArray +} + +global.EMIT_SESSION_STATUS = function (status, key) { + switch(status) { + case 'Running': { + global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Running') + break + } + case 'Stopped': { + global.EVENT_SERVER_CLIENT.raiseEvent(key, 'Stopped') + break + } + } } process.on('uncaughtException', function (err) { @@ -172,7 +426,8 @@ function preLoader() { global.EVENT_SERVER_CLIENT.listenToEvent('Task Server - ' + taskId, 'Run Task', undefined, 'Task Server - ' + taskId, undefined, eventReceived) global.EVENT_SERVER_CLIENT.raiseEvent('Task Manager - ' + taskId, 'Nodejs Process Ready for Task') function eventReceived(message) { - global.TASK_NODE = message + global.APP_SCHEMA_ARRAY = JSON.parse(message.event.appSchema) + setUpAppSchema() global.TASK_NODE = JSON.parse(message.event.taskDefinition) global.TASK_NETWORK = JSON.parse(message.event.networkDefinition) bootLoader() @@ -187,7 +442,8 @@ function preLoader() { //console.log('[INFO] Task Server -> server -> preLoader -> Waiting for event to start debugging...') global.EVENT_SERVER_CLIENT.listenToEvent('Task Server', 'Debug Task Started', undefined, 'Task Server', undefined, startDebugging) function startDebugging(message) { - global.TASK_NODE = message + global.APP_SCHEMA_ARRAY = JSON.parse(message.event.appSchema) + setUpAppSchema() global.TASK_NODE = JSON.parse(message.event.taskDefinition) global.TASK_NETWORK = JSON.parse(message.event.networkDefinition) bootLoader() @@ -197,6 +453,16 @@ function preLoader() { console.log('[ERROR] Task Server -> server -> preLoader -> global.TASK_NODE = ' + JSON.stringify(global.TASK_NODE).substring(0, 1000)) } } + + function setUpAppSchema() { + /* Setup the APP_SCHEMA_MAP based on the APP_SCHEMA_ARRAY */ + global.APP_SCHEMA_MAP = new Map() + for (let i = 0; i < global.APP_SCHEMA_ARRAY.length; i++) { + let nodeDefinition = global.APP_SCHEMA_ARRAY[i] + let key = nodeDefinition.type + global.APP_SCHEMA_MAP.set(key, nodeDefinition) + } + } } function bootLoader() { @@ -241,73 +507,85 @@ function bootLoader() { /* Validate that the minimun amount of input required are defined. */ - if (global.TASK_NODE.bot.processes[processIndex].marketReference === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Process Instance without a Market Reference. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); + if (global.TASK_NODE.parentNode === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Task without a Task Manager. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Market Reference without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference) + ", bot = " + global.TASK_NODE.bot.name); + if (global.TASK_NODE.parentNode.parentNode === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Task Manager without Mine Tasks. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.parentNode === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Market without a Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent)); + if (global.TASK_NODE.parentNode.parentNode.parentNode === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Mine Tasks without Market Tasks. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.parentNode.parentNode === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Exchange Markets without a Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.parentNode)); + if (global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Market Tasks without a Market. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.baseAsset === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Market without a Base Asset. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent)); + global.MARKET_NODE = global.TASK_NODE.parentNode.parentNode.parentNode.referenceParent + + if (global.MARKET_NODE.parentNode === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Market without a Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.MARKET_NODE)); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.quotedAsset === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Market without a Quoted Asset. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent)); + if (global.MARKET_NODE.parentNode.parentNode === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Exchange Markets without a Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.MARKET_NODE.parentNode)); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.baseAsset.referenceParent === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Base Asset without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.baseAsset)); + if (global.MARKET_NODE.baseAsset === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Market without a Base Asset. This process will not be executed. -> Process Instance = " + JSON.stringify(global.MARKET_NODE)); continue } - if (global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.quotedAsset.referenceParent === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Quoted Asset without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].marketReference.referenceParent.quotedAsset)); + if (global.MARKET_NODE.quotedAsset === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Market without a Quoted Asset. This process will not be executed. -> Process Instance = " + JSON.stringify(global.MARKET_NODE)); + continue + } + + if (global.MARKET_NODE.baseAsset.referenceParent === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Base Asset without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.MARKET_NODE.baseAsset)); + continue + } + + if (global.MARKET_NODE.quotedAsset.referenceParent === undefined) { + console.log("[ERROR] Task Server -> server -> bootLoader -> Quoted Asset without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.MARKET_NODE.quotedAsset)); continue } if (global.TASK_NODE.bot.processes[processIndex].referenceParent === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Process Instance without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); + console.log("[ERROR] Task Server -> server -> bootLoader -> Process Instance without a Reference Parent. This process will not be executed. -> Process Instance = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex])); continue } if (global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Process Definition without parent Bot Definition. -> Process Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent)); + console.log("[ERROR] Task Server -> server -> bootLoader -> Process Definition without parent Bot Definition. -> Process Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent)); continue } if (global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Bot Definition without parent Data Mine. -> Bot Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode)); + console.log("[ERROR] Task Server -> server -> bootLoader -> Bot Definition without parent Data Mine. -> Bot Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode)); continue } if (global.TASK_NODE.bot.processes[processIndex].referenceParent.config.codeName === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Process Definition without a codeName defined. -> Process Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent)); + console.log("[ERROR] Task Server -> server -> bootLoader -> Process Definition without a codeName defined. -> Process Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent)); continue } if (global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.config.codeName === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Bot Definition without a codeName defined. -> Bot Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode)); + console.log("[ERROR] Task Server -> server -> bootLoader -> Bot Definition without a codeName defined. -> Bot Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode)); continue } if (global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode.config.codeName === undefined) { - console.log("[WARN] Task Server -> server -> bootLoader -> Data Mine without a codeName defined. -> Data Mine Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode)); + console.log("[ERROR] Task Server -> server -> bootLoader -> Data Mine without a codeName defined. -> Data Mine Definition = " + JSON.stringify(global.TASK_NODE.bot.processes[processIndex].referenceParent.parentNode.parentNode)); continue }