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': { { /* 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.identifier.value === 'Closed') { continue } } /* Check if we need to Create this Order */ let situationName = checkOrderEvent(tradingSystemOrder.createOrderEvent, tradingSystemOrder, executionAlgorithm, executionNode) if (situationName !== undefined) { /* Open a new order */ await tryToOpenOrder(tradingEngineStage, executionAlgorithm, tradingSystemOrder, tradingEngineOrder, situationName) } } break } case 'Open': { /* Simulate Events that happens at the Exchange, if needed. */ simulateExchangeEvents(tradingEngineStage, tradingSystemOrder, tradingEngineOrder) /* Check Events that happens at the Exchange, if needed. */ await checkExchangeEvents(tradingEngineStage, tradingSystemOrder, tradingEngineOrder) /* 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 = 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') 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) { calculateOrderRate() 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) { return } /* Place Order at the Exchange, if needed. */ let result = await createOrderAtExchange(tradingSystemOrder, tradingEngineOrder) if (result !== true) { 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 function calculateOrderRate() { /* By default this is the order rate and it is the rate that applies to Market Orders */ tradingEngineOrder.rate.value = tradingEngine.current.episode.candle.close.value /* Optional Rate Definition */ if (tradingSystemOrder.orderRate !== undefined) { if (tradingSystemOrder.orderRate.formula !== undefined) { tradingEngineOrder.rate.value = tradingSystem.formulas.get(tradingSystemOrder.orderRate.formula.id) if (tradingEngineOrder.rate.value === undefined) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.rate.value === undefined', tradingEngineOrder) } if (isNaN(tradingEngineOrder.rate.value) === true) { badDefinitionUnhandledException(undefined, 'isNaN(tradingEngineOrder.rate.value) === true', tradingEngineOrder) } if (tradingEngineOrder.rate.value <= 0) { badDefinitionUnhandledException(undefined, 'tradingEngineOrder.rate.value <= 0', tradingEngineOrder) } tradingEngineOrder.rate.value = global.PRECISE(tradingEngineOrder.rate.value, 10) } } } 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 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 } } /* Filter by what is defined at the Strategy */ if (tradingSystemOrder.simulatedExchangeEvents === undefined) { badDefinitionUnhandledException(undefined, 'tradingSystemOrder.simulatedExchangeEvents === undefined', tradingSystemOrder) } 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 /* Based on the Trading System Definition */ 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 } } } } /* 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 * 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 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) { 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) } } else { /* 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) } } } function feesPaidSimulation() { /* The way the fess paid is 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. */ let calculatedBasedOnTradingSystem = false /* Based on the Trading System Definition */ if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid !== undefined) { if (tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage !== undefined) { if (tradingEngineOrder.orderBaseAsset.feesPaid.value === tradingEngineOrder.orderBaseAsset.feesPaid.config.initialValue) { tradingEngineOrder.orderBaseAsset.feesPaid.value = tradingEngineOrder.orderBaseAsset.size.value * tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage / 100 * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 calculatedBasedOnTradingSystem = true } if (tradingEngineOrder.orderQuotedAsset.feesPaid.value === tradingEngineOrder.orderQuotedAsset.feesPaid.config.initialValue) { tradingEngineOrder.orderQuotedAsset.feesPaid.value = tradingEngineOrder.orderQuotedAsset.size.value * tradingSystemOrder.simulatedExchangeEvents.simulatedFeesPaid.config.percentage / 100 * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 calculatedBasedOnTradingSystem = true } } } /* Based on the Session Parameters Definition */ if (calculatedBasedOnTradingSystem === false) { /* Fees are simulated based on the Session Paremeters */ switch (tradingEngineOrder.type) { case 'Market Order': { tradingEngineOrder.orderBaseAsset.feesPaid.value = tradingEngineOrder.orderBaseAsset.size.value * bot.SESSION.parameters.feeStructure.config.taker / 100 * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 tradingEngineOrder.orderQuotedAsset.feesPaid.value = tradingEngineOrder.orderQuotedAsset.size.value * bot.SESSION.parameters.feeStructure.config.taker / 100 * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 break } case 'Limit Order': { tradingEngineOrder.orderBaseAsset.feesPaid.value = tradingEngineOrder.orderBaseAsset.size.value * bot.SESSION.parameters.feeStructure.config.maker / 100 * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 tradingEngineOrder.orderQuotedAsset.feesPaid.value = tradingEngineOrder.orderQuotedAsset.size.value * bot.SESSION.parameters.feeStructure.config.maker / 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) } function sizeFilledSimulation() { /* Size Filled */ tradingEngineOrder.orderBaseAsset.sizeFilled.value = (tradingEngineOrder.orderBaseAsset.size.value - tradingEngineOrder.orderBaseAsset.feesPaid.value) * tradingEngineOrder.orderStatistics.percentageFilled.value / 100 tradingEngineOrder.orderQuotedAsset.sizeFilled.value = (tradingEngineOrder.orderQuotedAsset.size.value - tradingEngineOrder.orderQuotedAsset.feesPaid.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) { 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.episode.distanceToEvent.closeOrder.value = 1 updateEndsWithCycle(tradingEngineOrder) } 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 updateEndsWithCycle(tradingEngineOrder) } 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.episode.distanceToEvent.closeOrder.value = 1 updateEndsWithCycle(tradingEngineOrder) } syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) /* Forced Cancellation Check */ if (tradingEngineStage.status.value === 'Closing' && tradingEngineOrder.status.value !== 'Closed') { await exchangeCancelOrder(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, 'Closing Stage') } } function syncWithExchange(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) { let previousBaseAssetSizeFilled = tradingEngineOrder.orderBaseAsset.sizeFilled.value let previousQuotedAssetSizeFilled = tradingEngineOrder.orderQuotedAsset.sizeFilled.value let previousBaseAssetFeesPaid = tradingEngineOrder.orderBaseAsset.feesPaid.value let previousQuotedAssetFeesPaid = tradingEngineOrder.orderQuotedAsset.feesPaid.value /* Actual Rate Calculation */ tradingEngineOrder.orderStatistics.actualRate.value = order.price tradingEngineOrder.orderStatistics.actualRate.value = global.PRECISE(tradingEngineOrder.orderStatistics.actualRate.value, 10) /* Fees Paid Calculation */ /* As a response from the exchange we can not always get the fees. For that reason we need to estimate them base of the information that we do have. CCXT provides order.amount which represents the size we set for the order minus the fees taken by the exchange, all denominated in Base Asset. In this way if we substract to the order size this order.amount we can get the fees. All this is denominated in base asset because that is how CCXT works. The fees then can be estimated in Quoted Asset using the Actual Rate. */ tradingEngineOrder.orderBaseAsset.feesPaid.value = tradingEngineOrder.orderBaseAsset.size.value - order.amount tradingEngineOrder.orderBaseAsset.feesPaid.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.feesPaid.value, 10) tradingEngineOrder.orderQuotedAsset.feesPaid.value = tradingEngineOrder.orderBaseAsset.feesPaid.value * tradingEngineOrder.orderStatistics.actualRate.value tradingEngineOrder.orderQuotedAsset.feesPaid.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.feesPaid.value, 10) /* 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) /* Size Filled Calculation */ /* CCXT returns order.filled with an amount denominated in Base Asset. We will take it from there for our Order Base Asset. For our Order Quoted Asset we will use the field order.cost. */ tradingEngineOrder.orderBaseAsset.sizeFilled.value = order.filled tradingEngineOrder.orderBaseAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderBaseAsset.sizeFilled.value, 10) tradingEngineOrder.orderQuotedAsset.sizeFilled.value = order.cost tradingEngineOrder.orderQuotedAsset.sizeFilled.value = global.PRECISE(tradingEngineOrder.orderQuotedAsset.sizeFilled.value, 10) doTheAccounting( tradingEngineStage, tradingSystemOrder, tradingEngineOrder, previousBaseAssetSizeFilled, previousQuotedAssetSizeFilled, previousBaseAssetFeesPaid, previousQuotedAssetFeesPaid ) } function doTheAccounting( tradingEngineStage, tradingSystemOrder, tradingEngineOrder, previousBaseAssetSizeFilled, previousQuotedAssetSizeFilled, previousBaseAssetFeesPaid, previousQuotedAssetFeesPaid ) { updateStageAssets() updateBalances() 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) } function updateBalances() { /* Balances Update */ switch (true) { 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 /* 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 } 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 + 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 } } 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 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) { /* Close this Order */ tradingEngineOrder.status.value = 'Closed' tradingEngineOrder.exitType.value = exitType /* Initialize this */ tradingEngine.current.episode.distanceToEvent.closeOrder.value = 1 /* 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(tradingEngineStage, tradingSystemOrder, tradingEngineOrder, order) recalculateStageSize(tradingEngineStage) } } 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 + tradingEngineOrder.orderBaseAsset.feesPaid.value tradingEngineStage.stageQuotedAsset.sizePlaced.value = tradingEngineStage.stageQuotedAsset.sizePlaced.value + tradingEngineOrder.orderQuotedAsset.sizeFilled.value + tradingEngineOrder.orderQuotedAsset.feesPaid.value tradingEngineStage.stageBaseAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageBaseAsset.sizePlaced.value, 10) tradingEngineStage.stageQuotedAsset.sizePlaced.value = global.PRECISE(tradingEngineStage.stageQuotedAsset.sizePlaced.value, 10) } 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 } } 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 'It is not safe to continue with a Definition Error like this. Please fix the problem and try again.' } }